react-x11 2.10.2 → 2.12.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 +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -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 +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -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/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- 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 +334 -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 +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- 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 +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- 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,600 @@
|
|
|
1
|
+
// What flows down the tree: the theme and its tokens, the reading direction,
|
|
2
|
+
// the inherited text style and the `scale` zoom — and the walks that
|
|
3
|
+
// re-resolve a subtree when one of them moves.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyLayoutStyle,
|
|
7
|
+
textStyleFrom,
|
|
8
|
+
DEFAULT_TEXT_STYLE,
|
|
9
|
+
localTextStyleChanged,
|
|
10
|
+
resolvedTextDelta,
|
|
11
|
+
TEXT_REMEASURE,
|
|
12
|
+
} from '../styles.js';
|
|
13
|
+
import { Yoga } from '../yoga.js';
|
|
14
|
+
import { scaleOf } from '../scale.js';
|
|
15
|
+
import { baseTheme } from '../palette.js';
|
|
16
|
+
import { reportStyleError, STRICT_TOKENS } from '../errors.js';
|
|
17
|
+
import { DEV } from './util.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The desktop switched between light and dark: every node that inherited its
|
|
21
|
+
* palette rather than being given one has a different one now.
|
|
22
|
+
*
|
|
23
|
+
* `_themeChanged()` drops the cached theme and restyles the tokens; the
|
|
24
|
+
* invalidate is for everything else, above all the window background, which
|
|
25
|
+
* is read from the palette at paint time and belongs to no style object.
|
|
26
|
+
*
|
|
27
|
+
* Widgets do not need this — they read the palette through `useTheme()` and
|
|
28
|
+
* React re-renders them. This is the other route: an app's own
|
|
29
|
+
* `backgroundColor: '$background'`, and the window fill under it.
|
|
30
|
+
*/
|
|
31
|
+
export function appearanceChanged(app) {
|
|
32
|
+
// A desktop change arrives whenever the user makes it, which can be while
|
|
33
|
+
// the app is shutting down. An invalidate *schedules* a frame, so a repaint
|
|
34
|
+
// started here would reach the connection a tick after it closed and throw
|
|
35
|
+
// out of the frame clock, where nothing is waiting to catch it.
|
|
36
|
+
if (!app || app.X?._closing) return;
|
|
37
|
+
// A backend that renders native control bezels caches them by every
|
|
38
|
+
// parameter that changes the pixels — except the desktop's accent, which
|
|
39
|
+
// the toolkit reads for itself. The repaint below would blit the old
|
|
40
|
+
// colour back out of that cache, so it is forgotten first.
|
|
41
|
+
app.nativeBezels?.clear?.();
|
|
42
|
+
for (const node of app._rootChildren ?? []) {
|
|
43
|
+
if (node.destroyed) continue;
|
|
44
|
+
node._themeChanged();
|
|
45
|
+
node.root?.invalidate(true, null, 'theme');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Depth of the `_themeChanged` walk in progress, if any.
|
|
51
|
+
*
|
|
52
|
+
* That walk already visits every node in the subtree and re-resolves each
|
|
53
|
+
* one, so a style swap it performs on the way down must not kick off a
|
|
54
|
+
* second walk of the nodes it is about to reach anyway. Without the guard a
|
|
55
|
+
* theme change over a tree of token-using nodes is quadratic in its depth.
|
|
56
|
+
*/
|
|
57
|
+
export let inThemeWalk = 0;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A node's own `scale` prop as a factor: what it multiplies the scale it
|
|
61
|
+
* inherits by (`Node.scale`). Absent means 1, which is every node in a tree
|
|
62
|
+
* that never mentions it.
|
|
63
|
+
*
|
|
64
|
+
* A zero, a negative or a NaN is a mistake rather than a design — it would
|
|
65
|
+
* lay the subtree out at nothing, or at infinity — and in development it
|
|
66
|
+
* says so where the mistake is, rather than as a blank pane three frames
|
|
67
|
+
* later. Production falls back to 1 for the same reason a bad token keeps
|
|
68
|
+
* the property dropped: a GUI that carries on is worth more than one that
|
|
69
|
+
* dies on a fraction somebody divided by.
|
|
70
|
+
*/
|
|
71
|
+
function scaleFactorOf(props, kind) {
|
|
72
|
+
const own = props?.scale;
|
|
73
|
+
if (own === undefined) return 1;
|
|
74
|
+
if (typeof own === 'number' && Number.isFinite(own) && own > 0) return own;
|
|
75
|
+
if (DEV) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`react-x11: <${kind} scale={${JSON.stringify(own)}}> — a subtree ` +
|
|
78
|
+
'scale is a positive number, the factor this subtree is zoomed by ' +
|
|
79
|
+
'(2 draws it twice the size, 0.5 half), and leaving it out means 1. ' +
|
|
80
|
+
'See docs/scale.md, "A subtree of its own".',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Node's half of what flows down the tree, installed onto `Node.prototype` by node.js. */
|
|
87
|
+
export class NodeCascade {
|
|
88
|
+
/**
|
|
89
|
+
* Device pixels per logical pixel **for this node** — the display scale
|
|
90
|
+
* `createRoot` resolved (src/scale.js), times every `scale` prop between
|
|
91
|
+
* this node and its window. `this.style` and `this.abs` are already
|
|
92
|
+
* device pixels; this is for the values that never pass through a style —
|
|
93
|
+
* a paint constant like the caret's width, or an event coordinate on its
|
|
94
|
+
* way back to logical. A registered element that draws with its own
|
|
95
|
+
* constants multiplies them by this.
|
|
96
|
+
*
|
|
97
|
+
* The `scale` prop is CSS `zoom`, not a transform: it multiplies the
|
|
98
|
+
* inherited factor, and the node's *own* style scales with it. So
|
|
99
|
+
* everything downstream of the style funnel follows with no second
|
|
100
|
+
* mechanism — yoga lays out the scaled numbers like any others, paint
|
|
101
|
+
* reads the scaled style, the caret and the scrollbar read this getter,
|
|
102
|
+
* and text is shaped at the size it will be drawn at rather than
|
|
103
|
+
* rasterized once and stretched (docs/scale.md, "A subtree of its own").
|
|
104
|
+
*
|
|
105
|
+
* **A real X window is its own root.** `<window>` and `<popup>` geometry
|
|
106
|
+
* is the server's, in the display's pixels — `scaleWindowGeometry` reads
|
|
107
|
+
* `scaleOf(app)` directly and a WM sees no zoom — so the cascade stops
|
|
108
|
+
* there and a menu opened from a zoomed card comes up at the app's own
|
|
109
|
+
* size. Everything else inherits, including `<glarea>` and `<foreign>`,
|
|
110
|
+
* whose boxes are laid out by the parent like any other child's.
|
|
111
|
+
*
|
|
112
|
+
* Cached per node, dropped by `_rescaleSubtree` — the same contract
|
|
113
|
+
* `theme` and `direction` have, for the same reason.
|
|
114
|
+
*/
|
|
115
|
+
get scale() {
|
|
116
|
+
if (this._scaleCache !== undefined) return this._scaleCache;
|
|
117
|
+
if (this.isWindow) return (this._scaleCache = scaleOf(this.app));
|
|
118
|
+
const base = this.parent ? this.parent.scale : scaleOf(this.app);
|
|
119
|
+
return (this._scaleCache = base * scaleFactorOf(this.props, this.kind));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The effective scale of this node moved — its own `scale` prop changed,
|
|
124
|
+
* or it was attached under an ancestor whose scale is not the one it
|
|
125
|
+
* resolved against while detached.
|
|
126
|
+
*
|
|
127
|
+
* Everything below inherits, so the whole subtree is restyled; the early
|
|
128
|
+
* out is the answer not having moved, which is what makes the call at
|
|
129
|
+
* each of the attach sites free in the overwhelmingly common case of a
|
|
130
|
+
* tree with no `scale` prop in it at all.
|
|
131
|
+
*
|
|
132
|
+
* `mounting` is `insertBefore` attaching a subtree that has never been in
|
|
133
|
+
* the tree, and means the same thing it means to `_themeChanged`: resolve
|
|
134
|
+
* everything, claim nothing (issue #402). A node that has never painted
|
|
135
|
+
* has no stale pixels to cover, and where it lands is claimed by the
|
|
136
|
+
* child-list protocol.
|
|
137
|
+
*/
|
|
138
|
+
_rescaleSubtree(mounting = false) {
|
|
139
|
+
if (!this._rescaleMoved()) return;
|
|
140
|
+
// One claim for the whole walk: `_invalidateLayout` bounds the subtree
|
|
141
|
+
// as it stands and queues the after-layout claim, so the per-node
|
|
142
|
+
// repeats the recursion below would make are the same rect over again.
|
|
143
|
+
if (!mounting) this._invalidateLayout('scale');
|
|
144
|
+
this._rescaled(mounting);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Drop the cached scale and re-ask; true when the answer moved. The
|
|
148
|
+
* getter is the only place the rule lives, so a `<window>` — which
|
|
149
|
+
* resolves the display scale whatever it is written inside — answers
|
|
150
|
+
* false here and the walk stops at it. */
|
|
151
|
+
_rescaleMoved() {
|
|
152
|
+
const before = this._scaleCache;
|
|
153
|
+
this._scaleCache = undefined;
|
|
154
|
+
return this.scale !== before;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** …the walk itself, for a node whose scale is already known to have
|
|
158
|
+
* moved. Restyle in the new unit, then carry it down: an ordinary
|
|
159
|
+
* descendant's scale is a product of this one, so it moved too. */
|
|
160
|
+
_rescaled(mounting) {
|
|
161
|
+
// both hold a size in device pixels at the old scale
|
|
162
|
+
this._textBase = undefined;
|
|
163
|
+
this._textScaled = null;
|
|
164
|
+
const prevStyle = this.style;
|
|
165
|
+
const style = this._syncStyle(this.props, mounting);
|
|
166
|
+
if (this.yoga && style !== prevStyle) {
|
|
167
|
+
applyLayoutStyle(this.yoga, style, prevStyle);
|
|
168
|
+
}
|
|
169
|
+
if (localTextStyleChanged(style, prevStyle)) this._textContentChanged();
|
|
170
|
+
// its own claims are bounded, so this runs on a mount too — the same
|
|
171
|
+
// rule `_themeChanged` follows
|
|
172
|
+
this._retext();
|
|
173
|
+
for (const child of this.children) {
|
|
174
|
+
if (child._rescaleMoved()) child._rescaled(mounting);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The theme in force here: the nearest `theme` prop at or above this node,
|
|
180
|
+
* with an inner one merged over the outer so a panel can restate a colour
|
|
181
|
+
* or two without repeating a palette. Popups resolve through their place
|
|
182
|
+
* in the *tree*, not their window, so a menu inherits the theme of the UI
|
|
183
|
+
* that opened it even though it is a separate X window.
|
|
184
|
+
*
|
|
185
|
+
* **With no `theme` prop anywhere above, it is the desktop's palette.** So
|
|
186
|
+
* `backgroundColor: '$background'` works in an app that never wrote a
|
|
187
|
+
* `<ThemeProvider>`, and means "whatever this desktop's is" — which is the
|
|
188
|
+
* same answer `useTheme()` gives the widgets, by the other route.
|
|
189
|
+
*
|
|
190
|
+
* A detached node has no ancestors yet and so cannot see a provider two
|
|
191
|
+
* levels up; it still resolves, against the base, and `_themeChanged()` on
|
|
192
|
+
* attach re-resolves it against the real one.
|
|
193
|
+
*/
|
|
194
|
+
get theme() {
|
|
195
|
+
if (this._theme !== undefined) return this._theme;
|
|
196
|
+
const inherited = this.parent ? this.parent.theme : baseTheme();
|
|
197
|
+
const own = this.props.theme;
|
|
198
|
+
this._theme = own ? { ...inherited, ...own } : inherited;
|
|
199
|
+
return this._theme;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Which way this node reads: `'ltr'` or `'rtl'`, never `'inherit'` — this
|
|
204
|
+
* is the *resolved* answer, which is what everything outside yoga needs.
|
|
205
|
+
*
|
|
206
|
+
* Yoga resolves the same question for the layout on its own, and does it
|
|
207
|
+
* inside WASM where nothing can read it back (the binding has no
|
|
208
|
+
* `getComputedDirection`). So it is resolved a second time here, over the
|
|
209
|
+
* same rule, for everything the box tree does not answer: which side a
|
|
210
|
+
* scrollbar sits on, which physical edge a `borderStartWidth` paints, which
|
|
211
|
+
* way a popup flips, and the base direction a paragraph of neutral
|
|
212
|
+
* characters resolves against.
|
|
213
|
+
*
|
|
214
|
+
* The rule, nearest first:
|
|
215
|
+
*
|
|
216
|
+
* 1. `direction` in this node's own style — CSS's property, and the one
|
|
217
|
+
* thing that means "this subtree, whatever is around it".
|
|
218
|
+
* 2. otherwise the enclosing element's, which is what makes it inherit.
|
|
219
|
+
* 3. otherwise the palette's, which is seeded from the locale — so an app
|
|
220
|
+
* started in an RTL locale is mirrored without being asked, and
|
|
221
|
+
* `<ThemeProvider value={{ direction }}>` is how one with a language
|
|
222
|
+
* menu says otherwise. The provider plants the matching style property
|
|
223
|
+
* as it goes, so rule 1 is what actually carries a mid-tree swap and
|
|
224
|
+
* this clause is only ever read at the top of the tree.
|
|
225
|
+
*
|
|
226
|
+
* Cached like `theme` and dropped by the same walk, since the two now move
|
|
227
|
+
* together.
|
|
228
|
+
*/
|
|
229
|
+
get direction() {
|
|
230
|
+
if (this._direction !== undefined) return this._direction;
|
|
231
|
+
const own = this.style.direction;
|
|
232
|
+
return (this._direction =
|
|
233
|
+
own === 'ltr' || own === 'rtl'
|
|
234
|
+
? own
|
|
235
|
+
: this.parent
|
|
236
|
+
? this.parent.direction
|
|
237
|
+
: this.theme.direction === 'rtl'
|
|
238
|
+
? 'rtl'
|
|
239
|
+
: 'ltr');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The resolved direction moved — because this node's style named a new one,
|
|
244
|
+
* or because the palette under the whole tree did. Everything below
|
|
245
|
+
* inherits it, so the caches go with it, and the walk stops where a
|
|
246
|
+
* subtree states a direction of its own: nothing under that node can have
|
|
247
|
+
* changed.
|
|
248
|
+
*
|
|
249
|
+
* A node that never resolved one has nothing cached below it either —
|
|
250
|
+
* `direction` fills every ancestor on the way up — which is the same
|
|
251
|
+
* early-out the text cascade takes.
|
|
252
|
+
*/
|
|
253
|
+
_redirectSubtree() {
|
|
254
|
+
if (this._direction === undefined) return;
|
|
255
|
+
const before = this._direction;
|
|
256
|
+
this._direction = undefined;
|
|
257
|
+
if (this.direction === before) return;
|
|
258
|
+
this._directionMoved();
|
|
259
|
+
for (const child of this.children) child._redirectSubtree();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* What a direction change costs this node. The default is a repaint: the
|
|
264
|
+
* *layout* has already been dealt with by yoga, which was told about the
|
|
265
|
+
* style property directly, so what is left here is everything painted from
|
|
266
|
+
* the resolved side — the scrollbar, a logical border, an icon.
|
|
267
|
+
*
|
|
268
|
+
* `TextNode` overrides it: a paragraph's base direction is part of how it
|
|
269
|
+
* is shaped, so its cached layouts have to go.
|
|
270
|
+
*/
|
|
271
|
+
_directionMoved() {
|
|
272
|
+
this.root?.invalidate(false, this, 'direction');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The text style this node inherits — **the enclosing element's**, and at
|
|
277
|
+
* the top of the tree the palette's.
|
|
278
|
+
*
|
|
279
|
+
* The ink, the face and the size travel down the tree the way they do in
|
|
280
|
+
* CSS: `<box style={{ color: theme.textMuted, fontSize: 12 }}>` is how a caption
|
|
281
|
+
* block, a disabled row or a code panel is written, and it is what makes
|
|
282
|
+
* `color` on a row reach the row's label without the row handing it over.
|
|
283
|
+
* Only the properties in `INHERITED_TEXT_PROPS` travel; a style property on
|
|
284
|
+
* the node itself still wins, the way it always has.
|
|
285
|
+
*
|
|
286
|
+
* Under the last element is the palette, and that floor is not a set of
|
|
287
|
+
* constants either. The ink first: a `<text>` that never mentions a colour
|
|
288
|
+
* has to be readable on the surface it is drawn on, and that surface
|
|
289
|
+
* follows the desktop now. Black on `#1e2228` is invisible, which is the
|
|
290
|
+
* whole bug. The face and the size for the same reason one step out — a
|
|
291
|
+
* theme names `fontFamily` and `fontSize` because it is describing the type
|
|
292
|
+
* this app sets, and the only way that can be true is if the text nobody
|
|
293
|
+
* styled follows them.
|
|
294
|
+
*
|
|
295
|
+
* A detached node has no parent yet and so resolves against the floor;
|
|
296
|
+
* `insertBefore` re-resolves the subtree against the real one.
|
|
297
|
+
*/
|
|
298
|
+
get inheritedTextStyle() {
|
|
299
|
+
if (this.parent) {
|
|
300
|
+
const inherited = this.parent.resolvedTextStyle();
|
|
301
|
+
// A **scale boundary** — this node's `scale` prop, or a `<popup>`
|
|
302
|
+
// written inside a zoomed subtree, which goes back to the display's
|
|
303
|
+
// own unit. What comes down the cascade is already device pixels at
|
|
304
|
+
// the parent's scale (that is the point of resolving the theme's size
|
|
305
|
+
// once, at the root), so re-expressing it here is the only place a
|
|
306
|
+
// second multiply is right: a `fontSize: 14` theme inside a
|
|
307
|
+
// `scale={2}` box is 28 logical, 28 device at 1x, and the descendants
|
|
308
|
+
// below inherit that without compounding it again.
|
|
309
|
+
const from = this.parent.scale;
|
|
310
|
+
const to = this.scale;
|
|
311
|
+
if (from === to) return inherited;
|
|
312
|
+
const size = (inherited.size * to) / from;
|
|
313
|
+
const cached = this._textScaled;
|
|
314
|
+
if (cached?.from !== inherited || cached.style.size !== size) {
|
|
315
|
+
this._textScaled = { from: inherited, style: { ...inherited, size } };
|
|
316
|
+
}
|
|
317
|
+
return this._textScaled.style;
|
|
318
|
+
}
|
|
319
|
+
const theme = this.theme;
|
|
320
|
+
const color = theme.text;
|
|
321
|
+
// A palette can reach a node as a bare `theme` **prop** rather than a
|
|
322
|
+
// resolved one — `<box theme={{ text: 'red' }}>` merges and derives
|
|
323
|
+
// nothing (styling.md) — so neither of these is guaranteed to be there.
|
|
324
|
+
const family = theme.fontFamily ?? DEFAULT_TEXT_STYLE.family;
|
|
325
|
+
// The theme thinks in logical pixels like every style does, and this is
|
|
326
|
+
// the one door its font size enters the cascade by: a node's own
|
|
327
|
+
// `fontSize` was scaled at the style funnel, and every descendant
|
|
328
|
+
// inherits an already-resolved (device) size — so multiplying here,
|
|
329
|
+
// exactly once at the root of the cascade, is what keeps text and
|
|
330
|
+
// layout in the same unit without ever double-scaling (src/scale.js).
|
|
331
|
+
const size = (theme.fontSize ?? DEFAULT_TEXT_STYLE.size) * this.scale;
|
|
332
|
+
const base = this._textBase;
|
|
333
|
+
if (base?.color !== color || base.family !== family || base.size !== size) {
|
|
334
|
+
this._textBase = { ...DEFAULT_TEXT_STYLE, color, family, size };
|
|
335
|
+
}
|
|
336
|
+
return this._textBase;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Re-resolve this node's text style and pay for what moved.
|
|
341
|
+
*
|
|
342
|
+
* The cache is dropped rather than patched, and the *values* decide what
|
|
343
|
+
* happens next: a node that names its own `color` and face absorbs an
|
|
344
|
+
* ancestor's change entirely, which is what lets `_retextSubtree` stop
|
|
345
|
+
* walking there. Returns the cost so it can.
|
|
346
|
+
*
|
|
347
|
+
* A node with no cached resolution has never been asked, and neither has
|
|
348
|
+
* anything below it — `inheritedTextStyle` fills every ancestor on the way
|
|
349
|
+
* up, so an empty cache here proves an empty cache in the whole subtree.
|
|
350
|
+
* That is the early-out that keeps a hover on one row from touching a
|
|
351
|
+
* window's worth of nodes.
|
|
352
|
+
*/
|
|
353
|
+
_retext() {
|
|
354
|
+
const before = this._resolvedText;
|
|
355
|
+
if (before === undefined) return 0;
|
|
356
|
+
this._resolvedText = undefined;
|
|
357
|
+
const cost = resolvedTextDelta(before, this.resolvedTextStyle());
|
|
358
|
+
if (cost !== 0) this._textStyleMoved(cost);
|
|
359
|
+
return cost;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** …and everything under it, stopping wherever the answer did not change. */
|
|
363
|
+
_retextSubtree() {
|
|
364
|
+
if (this._retext() === 0) return;
|
|
365
|
+
for (const child of this.children) child._retextSubtree();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* This node's resolved text style moved — because its own style did, or
|
|
370
|
+
* because an ancestor's did and it inherited the change.
|
|
371
|
+
*
|
|
372
|
+
* `TEXT_REMEASURE` means a glyph can have moved and the box has to be
|
|
373
|
+
* measured again; `TEXT_REPAINT` means only the ink or the glyph rounding
|
|
374
|
+
* did, so the cached layout still has to go — the value rides on the spans
|
|
375
|
+
* inside it — but nothing reflows. Keeping those apart is the whole reason
|
|
376
|
+
* a `:hover { color }` costs a repaint rather than a layout pass.
|
|
377
|
+
*
|
|
378
|
+
* The default serves any element that draws text: re-measure if it has a
|
|
379
|
+
* size of its own, repaint otherwise. Elements that draw no text override
|
|
380
|
+
* it away.
|
|
381
|
+
*/
|
|
382
|
+
_textStyleMoved(cost) {
|
|
383
|
+
if (cost === TEXT_REMEASURE && this._measureFn) {
|
|
384
|
+
this.invalidateMeasure('text');
|
|
385
|
+
} else {
|
|
386
|
+
this.root?.invalidate(false, this, 'text');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Whether this node's ancestry is complete, so a `$token` that does not
|
|
392
|
+
* resolve is a mistake rather than a node that has not been placed yet.
|
|
393
|
+
*
|
|
394
|
+
* For a drawn node that is `root` — set by `_setRoot` when the subtree is
|
|
395
|
+
* attached to the window that owns it, which is exactly when no further
|
|
396
|
+
* `theme` prop can appear above it. React builds bottom-up, so having a
|
|
397
|
+
* *parent* proves nothing: the parent may itself be floating.
|
|
398
|
+
*
|
|
399
|
+
* The exception is a `<popup>`, which is its own root from the moment it is
|
|
400
|
+
* created and only learns where in the tree it was written when it is
|
|
401
|
+
* attached. Until then its subtree would be judged against the base palette
|
|
402
|
+
* alone, and a `$panel` from the provider two levels up would throw. Those
|
|
403
|
+
* resolve provisionally instead, and `_themeChanged()` on attach re-resolves
|
|
404
|
+
* them. Known tokens resolve either way, so only the *error* is ever
|
|
405
|
+
* deferred, never the value.
|
|
406
|
+
*/
|
|
407
|
+
get placed() {
|
|
408
|
+
const owner = this.isWindow ? this : this.root;
|
|
409
|
+
if (!owner) return false;
|
|
410
|
+
return owner.isPopup ? owner.parent != null : true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A `$token` this node's completed ancestry does not define.
|
|
415
|
+
*
|
|
416
|
+
* The default is `reportStyleError`: say so loudly, set `process.exitCode`,
|
|
417
|
+
* and keep the property dropped. `REACT_X11_STRICT_TOKENS=1` makes it fatal
|
|
418
|
+
* again, and then *where* the throw lands is the whole question — an error
|
|
419
|
+
* boundary only catches what React invoked, on the fiber React thinks it
|
|
420
|
+
* is working on.
|
|
421
|
+
*
|
|
422
|
+
* `mounting` is the attach walk, which runs inside `appendInitialChild`
|
|
423
|
+
* while React is completing the nearest host *ancestor* — the `<window>`,
|
|
424
|
+
* for a whole tree rendered at once. A throw there is attributed to the
|
|
425
|
+
* window and sails past every boundary the app wrote inside it, which is
|
|
426
|
+
* the bug this deferral exists for (#420). Stashed instead, and thrown
|
|
427
|
+
* from `commitMount` on this node's own fiber, where the walk up finds a
|
|
428
|
+
* boundary at any depth.
|
|
429
|
+
*
|
|
430
|
+
* Every other caller already has the right fiber (`commitUpdate`) or has
|
|
431
|
+
* no React on the stack at all (`appearanceChanged`, from an X event) —
|
|
432
|
+
* for those, throwing here is both the earliest and the only option, and
|
|
433
|
+
* the second is the crash strict mode asked for.
|
|
434
|
+
*
|
|
435
|
+
* `commitMount` happens once per instance, so a node re-attached after it
|
|
436
|
+
* has been and gone has nothing left to defer *to*; stashing there would
|
|
437
|
+
* swallow the error instead of raising it late. Those throw at once, like
|
|
438
|
+
* the keyed reorder they resemble.
|
|
439
|
+
*/
|
|
440
|
+
_tokenProblem(problems, mounting, consequence = undefined) {
|
|
441
|
+
if (!STRICT_TOKENS) {
|
|
442
|
+
// every one of them: two misspellings in a style are two things to
|
|
443
|
+
// fix, and a report that named only the first would send someone back
|
|
444
|
+
// for a second run to find the second
|
|
445
|
+
for (const message of problems) {
|
|
446
|
+
reportStyleError(this, message, consequence);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const error = new Error(problems[0]);
|
|
451
|
+
if (mounting && this._tokenError === null) this._tokenError = error;
|
|
452
|
+
else throw error;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The theme above or on this node changed: drop the caches and restyle
|
|
456
|
+
* the subtree, since a token can appear at any depth.
|
|
457
|
+
*
|
|
458
|
+
* `mounting` is `insertBefore` attaching a subtree that has never been in
|
|
459
|
+
* the tree: the walk still resolves every token — the nodes can see their
|
|
460
|
+
* ancestors now — but it claims no damage (issue #402). A node that has
|
|
461
|
+
* never painted has no stale pixels to cover, and the rect it is about to
|
|
462
|
+
* occupy is claimed by the child-list/layout-diff protocol like any other
|
|
463
|
+
* inserted child's; the unbounded claims below would turn every commit
|
|
464
|
+
* that mounts a token-styled node into a full-window repaint — which is
|
|
465
|
+
* every re-slice of a virtualized list whose rows follow the palette. A
|
|
466
|
+
* live theme *swap* is the other caller and keeps them: it moves pixels
|
|
467
|
+
* that are already on screen, anywhere in the subtree. */
|
|
468
|
+
_themeChanged(mounting = false) {
|
|
469
|
+
// This walk visits every node itself, so the per-node re-resolution below
|
|
470
|
+
// is enough — a style swap it causes must not start a second walk of the
|
|
471
|
+
// same subtree from halfway down.
|
|
472
|
+
inThemeWalk++;
|
|
473
|
+
try {
|
|
474
|
+
this._theme = undefined;
|
|
475
|
+
// The palette is the floor under the direction too, and this walk
|
|
476
|
+
// already visits every node — so the cache is dropped here rather than
|
|
477
|
+
// through `_redirectSubtree`, which would walk the same subtree again.
|
|
478
|
+
const wasDirection = this._direction;
|
|
479
|
+
this._direction = undefined;
|
|
480
|
+
// A `<window>` with no `backgroundColor` of its own follows the palette,
|
|
481
|
+
// and the server's copy of that colour has to follow with it — otherwise
|
|
482
|
+
// the next resize fills the new area in the old scheme.
|
|
483
|
+
if (this.isWindow) this._syncWindowBackground();
|
|
484
|
+
if (this._usesTokens) {
|
|
485
|
+
const before = this.style;
|
|
486
|
+
this._syncStyle(this.props, mounting);
|
|
487
|
+
// a token change reaches the node without React re-rendering it, so
|
|
488
|
+
// the invalidation a commit would have done has to happen here too
|
|
489
|
+
if (localTextStyleChanged(this.style, before)) {
|
|
490
|
+
this._textContentChanged();
|
|
491
|
+
}
|
|
492
|
+
if (!mounting) this.root?.invalidate(true, null, 'theme');
|
|
493
|
+
}
|
|
494
|
+
// The palette is the floor under the cascade, so a theme swap moves the
|
|
495
|
+
// resolved style of every node that named none of its own — and none of
|
|
496
|
+
// that is in a style object, so nothing above would have noticed. A
|
|
497
|
+
// swap that only changes `fontFamily` is the case that made this worth
|
|
498
|
+
// having: nothing else about the node changes, and a cached layout
|
|
499
|
+
// carries the face it was shaped with. `_retext` runs on a mount too —
|
|
500
|
+
// its own claims are bounded — but cannot answer non-zero there: a
|
|
501
|
+
// node that was never attached has never resolved a text style.
|
|
502
|
+
if (this._retext() !== 0 && !mounting) {
|
|
503
|
+
this.root?.invalidate(true, null, 'theme');
|
|
504
|
+
}
|
|
505
|
+
if (wasDirection !== undefined && this.direction !== wasDirection) {
|
|
506
|
+
this._directionMoved();
|
|
507
|
+
}
|
|
508
|
+
for (const child of this.children) child._themeChanged(mounting);
|
|
509
|
+
} finally {
|
|
510
|
+
inThemeWalk--;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The text style this node resolves to, in the shape `app.fonts.layout`
|
|
516
|
+
* takes as its base: `{ family, size, weight, style, variations,
|
|
517
|
+
* textRendering, color }`. `<text>`, `<textinput>` and the document views
|
|
518
|
+
* draw with exactly this, and an element that draws text of its own is
|
|
519
|
+
* asking the same question they are.
|
|
520
|
+
*
|
|
521
|
+
* Two things are folded in that `this.style` does not carry. The palette
|
|
522
|
+
* is under it — `text`, `fontFamily` and `fontSize` are the ink, the face
|
|
523
|
+
* and the size of everything that named none of its own (styling.md) — so
|
|
524
|
+
* an element reading its own style alone is one whose app can say
|
|
525
|
+
* `<ThemeProvider value={{ fontFamily: 'Inter' }}>` and watch it reach
|
|
526
|
+
* every built-in label and stop at this one. And the bag is spelled ntk's
|
|
527
|
+
* way rather than the style vocabulary's (`family`, not `fontFamily`;
|
|
528
|
+
* `variations`, not `fontVariationSettings`), which is a mapping worth
|
|
529
|
+
* having in one place instead of vendored per element.
|
|
530
|
+
*
|
|
531
|
+
* **Cached**, and the cache is the cascade's spine: asking here fills every
|
|
532
|
+
* ancestor's on the way up, which is what lets an invalidation walk stop at
|
|
533
|
+
* a node that never resolved (`_retext`). Everything that can move the
|
|
534
|
+
* answer drops it — a style swap (`_retarget`), an animation tick, a theme
|
|
535
|
+
* change, an attach — so an element may keep reading it at paint time.
|
|
536
|
+
*/
|
|
537
|
+
resolvedTextStyle() {
|
|
538
|
+
return (this._resolvedText ??= textStyleFrom(
|
|
539
|
+
this.style,
|
|
540
|
+
this.inheritedTextStyle,
|
|
541
|
+
));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** WindowNode's half of what flows down the tree, installed onto `WindowNode.prototype` by window/window.js. */
|
|
546
|
+
export class WindowCascade {
|
|
547
|
+
/**
|
|
548
|
+
* The smallest size this window's content can be drawn at: GTK's
|
|
549
|
+
* `minimum` to the `natural` below, and what an `'auto'` `minWidth` or
|
|
550
|
+
* `minHeight` resolves to.
|
|
551
|
+
*
|
|
552
|
+
* One layout pass **with no space on offer at all**, which is the whole
|
|
553
|
+
* trick — every node comes out at the smallest size its own style allows,
|
|
554
|
+
* text measures at its longest word, and a wrapping row wraps at every
|
|
555
|
+
* item. `contentSpan` then reads how far that reached, recovering a node
|
|
556
|
+
* the pass squashed by looking inside it.
|
|
557
|
+
*
|
|
558
|
+
* What it deliberately does *not* do is second-guess that layout. A node
|
|
559
|
+
* that said how small it can be — `minWidth: 0`, or an `overflow` that
|
|
560
|
+
* clips — is taken at its word and its content stops counting. That is
|
|
561
|
+
* CSS's `min-width: 0`, Qt's `QScrollArea` and GTK's `min-content-width`,
|
|
562
|
+
* and a scroll container gets it here for free.
|
|
563
|
+
*
|
|
564
|
+
* `forWidth` is the width the height is measured for, and there has to be
|
|
565
|
+
* one: a paragraph's minimum height is a height *for a width*. GTK asks
|
|
566
|
+
* for its minimum height at its minimum width, which for a paragraph is
|
|
567
|
+
* the width it is tallest at — an honest answer to a question nobody
|
|
568
|
+
* asked, since the window is not at its minimum width. `WM_NORMAL_HINTS`
|
|
569
|
+
* holds two independent numbers and cannot express the dependency either
|
|
570
|
+
* way, so the floor is measured at the width the window will actually
|
|
571
|
+
* have and re-sent as that changes.
|
|
572
|
+
*/
|
|
573
|
+
/**
|
|
574
|
+
* The direction to lay this window's tree out in, as yoga spells it.
|
|
575
|
+
*
|
|
576
|
+
* `calculateLayout`'s third argument is the direction the *owner* imposes,
|
|
577
|
+
* and a window has no owner — so the root reads its own resolved value and
|
|
578
|
+
* hands it down. A `direction` on a `<box>` inside is then yoga's business
|
|
579
|
+
* rather than ours: it carries the property on its own node and everything
|
|
580
|
+
* under it inherits from there.
|
|
581
|
+
*/
|
|
582
|
+
get _rootDirection() {
|
|
583
|
+
return this.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The root's direction is an argument to `calculateLayout` rather than a
|
|
588
|
+
* property on a yoga node, so nothing in the box tree is dirty when it
|
|
589
|
+
* moves and the layout pass has to be asked for. That is only reachable
|
|
590
|
+
* from the palette — a `direction` written in the window's own style goes
|
|
591
|
+
* through `applyLayoutStyle`, which dirties the node the ordinary way.
|
|
592
|
+
*/
|
|
593
|
+
_directionMoved() {
|
|
594
|
+
this.invalidate(true, null, 'direction');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Like a `<box>`: a window fills and never letters, so it is the top of
|
|
598
|
+
* the cascade rather than a reader of it. */
|
|
599
|
+
_textStyleMoved() {}
|
|
600
|
+
}
|