react-x11 2.15.3 → 2.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/package.json +3 -3
- package/src/Reconciler.js +85 -22
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +79 -19
- package/src/capabilities.js +29 -4
- package/src/cocoa/app.js +211 -11
- package/src/cocoa/context2d.js +23 -0
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/desktopcapabilityhooks.js +29 -6
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +83 -1
- package/src/index.d.ts +10 -1
- package/src/index.js +3 -0
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +63 -1
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/screens.js +159 -24
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +164 -17
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +21 -5
- package/src/types/capabilities.d.ts +13 -1
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/events.d.ts +5 -0
- package/src/types/filedialog.d.ts +3 -1
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +14 -2
- package/src/wayland/xkb.js +170 -59
- package/src/windowid.js +62 -20
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Where a `<ThemeProvider>` meets the windows. At the root of the tree: what
|
|
2
|
+
// the container holds — top-level windows and popups — and the node a
|
|
3
|
+
// provider written above them becomes. And directly inside a window: the box
|
|
4
|
+
// a provider becomes there, which passes a nested window on to that window.
|
|
5
|
+
|
|
6
|
+
import { hooks as a11yHooks } from '../a11y.js';
|
|
7
|
+
import { baseTheme } from '../palette.js';
|
|
8
|
+
import { BoxNode } from './box.js';
|
|
9
|
+
import { THEME_SCOPE } from './kinds.js';
|
|
10
|
+
import { Node } from './node.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Put `node` at the top of the tree: realize a window against the screen
|
|
14
|
+
* root, and record it among the container's top-level windows — the list
|
|
15
|
+
* every "which windows does this app have" question reads
|
|
16
|
+
* (`app._rootChildren`). A theme scope puts its windows there instead of
|
|
17
|
+
* itself, so the list stays one of windows.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent, because React reorders a keyed list at the root by inserting
|
|
20
|
+
* a child that is already mounted: a second entry for the same window would
|
|
21
|
+
* be a window counted twice, and announced to assistive technology twice.
|
|
22
|
+
*/
|
|
23
|
+
export function attachTopLevel(app, node) {
|
|
24
|
+
if (node.isThemeScope) {
|
|
25
|
+
node._attach();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// realize the whole subtree top-down against the screen root
|
|
29
|
+
if (!node.window) node.realize(null);
|
|
30
|
+
// React's getPublicRootInstance answers from the root fiber's first
|
|
31
|
+
// child, and only when that child is a host component. `render()` wraps
|
|
32
|
+
// the tree in a context provider, which is not one, so it would answer
|
|
33
|
+
// null — the container keeps the list instead.
|
|
34
|
+
const roots = (app._rootChildren ??= []);
|
|
35
|
+
if (roots.includes(node)) return;
|
|
36
|
+
roots.push(node);
|
|
37
|
+
a11yHooks.rootMounted?.(node);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Take `node` back off the top of the tree, before its subtree is
|
|
41
|
+
* destroyed — the bridge reads the subtree to say what went. */
|
|
42
|
+
export function detachTopLevel(app, node) {
|
|
43
|
+
if (node.isThemeScope) {
|
|
44
|
+
node._detach();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const roots = app._rootChildren;
|
|
48
|
+
const at = roots ? roots.indexOf(node) : -1;
|
|
49
|
+
if (at !== -1) roots.splice(at, 1);
|
|
50
|
+
a11yHooks.rootUnmounted?.(node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `<ThemeProvider>` above the windows (#584).
|
|
55
|
+
*
|
|
56
|
+
* A provider has to put its palette on a node, because a `$token` resolves by
|
|
57
|
+
* walking the node tree and knows nothing about React context. Inside a window
|
|
58
|
+
* that node is a `<box>`. At the root it cannot be — nothing drawn can be
|
|
59
|
+
* there, and a window cannot be inside a box — so the provider used to put
|
|
60
|
+
* the palette on the windows it could see among its children instead. It
|
|
61
|
+
* could only see literal `<window>` elements: a component that renders one,
|
|
62
|
+
* a window that is closed, a fragment of two, all planted a box at the root.
|
|
63
|
+
*
|
|
64
|
+
* This is the node for that position. It draws nothing, lays nothing out and
|
|
65
|
+
* holds only windows, popups and other scopes, and the windows under it take
|
|
66
|
+
* their palette from it: a top-level window keeps **no parent** — a window
|
|
67
|
+
* with a parent is a nested one to everything that asks, the accessibility
|
|
68
|
+
* bridge first — and reads the scope through `_scope` instead
|
|
69
|
+
* (`Node.theme`).
|
|
70
|
+
*
|
|
71
|
+
* The scope itself is never in `app._rootChildren`; its windows are, from
|
|
72
|
+
* the moment it is attached to the container (`attachTopLevel`).
|
|
73
|
+
*/
|
|
74
|
+
export class ThemeScopeNode extends Node {
|
|
75
|
+
constructor(props, app) {
|
|
76
|
+
super(THEME_SCOPE, props, app, { yoga: false });
|
|
77
|
+
this.isThemeScope = true;
|
|
78
|
+
// the scope this one is written inside, when providers nest at the root
|
|
79
|
+
this._scope = null;
|
|
80
|
+
// in the container: its windows are realized and on the root list
|
|
81
|
+
this._attached = false;
|
|
82
|
+
// React's hide (`<Suspense>`, `<Activity>`), which lands on the topmost
|
|
83
|
+
// host instance under the boundary — this, when the provider is there
|
|
84
|
+
this._reactHidden = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The palette the windows under this scope inherit: this scope's own
|
|
89
|
+
* `theme` over the scope around it, or over the desktop's palette.
|
|
90
|
+
*
|
|
91
|
+
* Not cached, unlike `Node.theme`. Nothing tells a scope that the
|
|
92
|
+
* desktop's palette moved — `appearanceChanged` walks the windows — and a
|
|
93
|
+
* cached merge would hand them the old base. Each window caches what it
|
|
94
|
+
* read, so this runs once per window per theme change.
|
|
95
|
+
*/
|
|
96
|
+
get theme() {
|
|
97
|
+
const inherited = this._scope ? this._scope.theme : baseTheme();
|
|
98
|
+
const own = this.props.theme;
|
|
99
|
+
return own ? { ...inherited, ...own } : inherited;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
insertBefore(child, beforeChild) {
|
|
103
|
+
const from = this.children.indexOf(child);
|
|
104
|
+
if (from !== -1) this.children.splice(from, 1);
|
|
105
|
+
const before =
|
|
106
|
+
beforeChild == null ? -1 : this.children.indexOf(beforeChild);
|
|
107
|
+
this.children.splice(
|
|
108
|
+
before === -1 ? this.children.length : before,
|
|
109
|
+
0,
|
|
110
|
+
child,
|
|
111
|
+
);
|
|
112
|
+
// a keyed reorder: the child is already linked, themed and attached
|
|
113
|
+
if (from !== -1) return;
|
|
114
|
+
child._scope = this;
|
|
115
|
+
// Built while detached, the subtree resolved its tokens against the
|
|
116
|
+
// desktop's palette; this is the attach walk that re-resolves them, the
|
|
117
|
+
// same one `Node.insertBefore` runs — a mount, so it claims no damage.
|
|
118
|
+
child._themeChanged(true);
|
|
119
|
+
if (this._hiddenByReact()) child._applyHidden();
|
|
120
|
+
if (this._attached) attachTopLevel(this.app, child);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
removeChild(child) {
|
|
124
|
+
const at = this.children.indexOf(child);
|
|
125
|
+
if (at === -1) return;
|
|
126
|
+
this.children.splice(at, 1);
|
|
127
|
+
if (this._attached) detachTopLevel(this.app, child);
|
|
128
|
+
child._scope = null;
|
|
129
|
+
child.destroySubtree();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
applyProps(newProps, oldProps) {
|
|
133
|
+
const before = oldProps ?? this.props;
|
|
134
|
+
this.props = newProps;
|
|
135
|
+
// A swap walks the windows as a live change, which repaints them. The
|
|
136
|
+
// `style` a provider passes lays out a box inside a window and means
|
|
137
|
+
// nothing here: a direction it names is in the palette as well, which
|
|
138
|
+
// is what a window reads its direction from.
|
|
139
|
+
if (newProps.theme !== before.theme) this._themeChanged();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setHidden(hidden) {
|
|
143
|
+
this._reactHidden = hidden;
|
|
144
|
+
this._applyHidden();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The hidden state above the windows moved: each one re-derives its own. */
|
|
148
|
+
_applyHidden() {
|
|
149
|
+
for (const child of this.children) child._applyHidden();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Whether React hides this scope, here or at a scope around it. */
|
|
153
|
+
_hiddenByReact() {
|
|
154
|
+
return this._reactHidden || (this._scope?._hiddenByReact() ?? false);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_attach() {
|
|
158
|
+
this._attached = true;
|
|
159
|
+
for (const child of this.children) attachTopLevel(this.app, child);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_detach() {
|
|
163
|
+
this._attached = false;
|
|
164
|
+
for (const child of this.children) detachTopLevel(this.app, child);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `<ThemeProvider>` written directly inside a `<window>` (or a `<popup>`).
|
|
170
|
+
*
|
|
171
|
+
* There it is a box that fills its parent, like a provider anywhere else in
|
|
172
|
+
* a window — but a window may nest windows, and a provider wrapped around one
|
|
173
|
+
* has to hand it on, since a window cannot live inside a box. So a nested
|
|
174
|
+
* window put under this box is **passed through** to the window the box is
|
|
175
|
+
* in: that window holds it, realizes and stacks it the way it does any
|
|
176
|
+
* nested window, and the window reads its palette from here through
|
|
177
|
+
* `_scope`, which a palette lookup asks before the parent (`Node.theme`).
|
|
178
|
+
*
|
|
179
|
+
* React still sees the window as this box's child, so the calls it makes on
|
|
180
|
+
* the box for that window — a move, a removal — are forwarded as well, and
|
|
181
|
+
* the box answers for the passed windows in the three things it owns over its
|
|
182
|
+
* subtree: a theme swap, a hide, and its own removal.
|
|
183
|
+
*
|
|
184
|
+
* Only here. Under a provider that is inside a `<box>` there is no window to
|
|
185
|
+
* pass a window to, and `Node.insertBefore` says so.
|
|
186
|
+
*/
|
|
187
|
+
export class ThemeBoxNode extends BoxNode {
|
|
188
|
+
constructor(props, app) {
|
|
189
|
+
super(props, app);
|
|
190
|
+
// the nested windows React put under this box, in the order it did
|
|
191
|
+
this._windows = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
insertBefore(child, beforeChild) {
|
|
195
|
+
if (!child.isWindow || child.isPopup) {
|
|
196
|
+
super.insertBefore(child, beforeChild);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!this._windows.includes(child)) {
|
|
200
|
+
this._windows.push(child);
|
|
201
|
+
child._scope = this;
|
|
202
|
+
}
|
|
203
|
+
// Before the box is in its window the window waits here, and `_setRoot`
|
|
204
|
+
// passes it on at the attach. A move is re-inserted at the end: nested
|
|
205
|
+
// windows stack among themselves, not among the box's drawn children.
|
|
206
|
+
if (this.parent) this.parent.insertBefore(child, null);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
removeChild(child) {
|
|
210
|
+
const at = this._windows.indexOf(child);
|
|
211
|
+
if (at === -1) {
|
|
212
|
+
super.removeChild(child);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this._windows.splice(at, 1);
|
|
216
|
+
// the window destroys it, and takes it off its stacking list
|
|
217
|
+
if (child.parent) child.parent.removeChild(child);
|
|
218
|
+
else child.destroySubtree();
|
|
219
|
+
child._scope = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_setRoot(root) {
|
|
223
|
+
const attaching = this.root !== root;
|
|
224
|
+
super._setRoot(root);
|
|
225
|
+
if (!attaching || !this.parent) return;
|
|
226
|
+
for (const win of this._windows) {
|
|
227
|
+
if (win.parent !== this.parent) this.parent.insertBefore(win, null);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_themeChanged(mounting = false) {
|
|
232
|
+
super._themeChanged(mounting);
|
|
233
|
+
for (const win of this._windows) win._themeChanged(mounting);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setHidden(hidden) {
|
|
237
|
+
super.setHidden(hidden);
|
|
238
|
+
for (const win of this._windows) win._applyHidden();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** React hid this box — the hide a `<Suspense>` around the provider
|
|
242
|
+
* lands on, which the windows passed on from here have to follow. */
|
|
243
|
+
_hiddenByReact() {
|
|
244
|
+
return this.hidden;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
destroySubtree() {
|
|
248
|
+
super.destroySubtree();
|
|
249
|
+
// React removes the box alone, never the windows under it, so they go
|
|
250
|
+
// with it — out of the window that holds them, which is each one's
|
|
251
|
+
// parent (the box's own is already cleared by now), unless that window
|
|
252
|
+
// is being destroyed itself and is walking its children as it does.
|
|
253
|
+
for (const win of this._windows) {
|
|
254
|
+
const holder = win.parent;
|
|
255
|
+
if (holder && !holder.destroyed) holder.removeChild(win);
|
|
256
|
+
else if (!win.destroyed) win.destroySubtree();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -59,6 +59,17 @@ function replayHeld({ base, steps }, max) {
|
|
|
59
59
|
// walked per node per hit test.
|
|
60
60
|
const EMPTY_SCROLLBARS = Object.freeze([]);
|
|
61
61
|
|
|
62
|
+
/** Do two answers from `_scrollbar` paint the same thumb? Neither painting
|
|
63
|
+
* one counts. */
|
|
64
|
+
const sameThumb = (a, b) =>
|
|
65
|
+
a === b ||
|
|
66
|
+
(a !== undefined &&
|
|
67
|
+
b !== undefined &&
|
|
68
|
+
a.x === b.x &&
|
|
69
|
+
a.y === b.y &&
|
|
70
|
+
a.width === b.width &&
|
|
71
|
+
a.height === b.height);
|
|
72
|
+
|
|
62
73
|
/**
|
|
63
74
|
* Scrolling, as a style rather than as a species of node.
|
|
64
75
|
*
|
|
@@ -177,6 +188,9 @@ export const Scrollable = (Base) =>
|
|
|
177
188
|
this._childOrigin != null &&
|
|
178
189
|
!this._scrollMeasureDirty &&
|
|
179
190
|
!this.yoga.hasNewLayout();
|
|
191
|
+
// The thumbs as they stand, for a bounded frame to hold the measure
|
|
192
|
+
// below against (`_claimThumbs`)
|
|
193
|
+
const thumbsWere = !clean && layoutDiff.sink ? this._scrollbars() : null;
|
|
180
194
|
if (!clean) {
|
|
181
195
|
const size = this.measureScrollContent();
|
|
182
196
|
if (!Number.isFinite(size?.width) || !Number.isFinite(size?.height)) {
|
|
@@ -208,8 +222,13 @@ export const Scrollable = (Base) =>
|
|
|
208
222
|
// once and scrollIntoView waits for the pass.
|
|
209
223
|
this._resolveScrollTo();
|
|
210
224
|
this._resolveScrollIntoView();
|
|
225
|
+
const askedX = this.scrollX;
|
|
226
|
+
const askedY = this.scrollY;
|
|
211
227
|
this.scrollY = clampScroll(this.scrollY, this._maxScroll('y'));
|
|
212
228
|
this.scrollX = clampScroll(this.scrollX, this._maxScroll('x'));
|
|
229
|
+
// Of those three moves the clamp's is the one no call claimed when it
|
|
230
|
+
// was asked for — the diff below claims it
|
|
231
|
+
const clamped = this.scrollX !== askedX || this.scrollY !== askedY;
|
|
213
232
|
this._reportViewport();
|
|
214
233
|
this._reportScrollTo(from);
|
|
215
234
|
// `scrollX` is how far the content has moved **from its start**, which
|
|
@@ -271,14 +290,22 @@ export const Scrollable = (Base) =>
|
|
|
271
290
|
}
|
|
272
291
|
};
|
|
273
292
|
layoutDiff.shift = { x: ox - wasOrigin.x, y: oy - wasOrigin.y };
|
|
274
|
-
} else if (shifted) {
|
|
275
|
-
layoutDiff.sink = null;
|
|
276
293
|
} else {
|
|
277
294
|
const vp = insetRect(this.abs, -DAMAGE_SLOP);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
if (shifted) {
|
|
296
|
+
layoutDiff.sink = null;
|
|
297
|
+
// …which holds for a scroll's shift and not for the clamp's: the
|
|
298
|
+
// content shrank, or the viewport grew, under the offset — a row
|
|
299
|
+
// collapsing at the end of a list scrolled to its end — and
|
|
300
|
+
// nothing claimed the viewport every child just moved in.
|
|
301
|
+
if (clamped) outer(vp);
|
|
302
|
+
} else {
|
|
303
|
+
layoutDiff.sink = (rect) => {
|
|
304
|
+
const clipped = intersectRects(rect, vp);
|
|
305
|
+
if (clipped) outer(clipped);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
this._claimThumbs(thumbsWere, outer);
|
|
282
309
|
}
|
|
283
310
|
}
|
|
284
311
|
try {
|
|
@@ -818,6 +845,26 @@ export const Scrollable = (Base) =>
|
|
|
818
845
|
return [this._scrollbar('y'), this._scrollbar('x')].filter(Boolean);
|
|
819
846
|
}
|
|
820
847
|
|
|
848
|
+
/**
|
|
849
|
+
* A bounded layout pass's claim for the thumbs it changed, where each
|
|
850
|
+
* was and where it is. The pane paints them, not a node with a rect of
|
|
851
|
+
* its own, so content growing or shrinking under a pane that stays put
|
|
852
|
+
* slides a thumb or resizes it and the layout diff sees nothing move.
|
|
853
|
+
* `was` is `_scrollbars()` from before the pass measured the content.
|
|
854
|
+
*/
|
|
855
|
+
_claimThumbs(was, sink) {
|
|
856
|
+
const now = this._scrollbars();
|
|
857
|
+
for (const axis of ['y', 'x']) {
|
|
858
|
+
const before = was.find((bar) => bar.axis === axis);
|
|
859
|
+
const after = now.find((bar) => bar.axis === axis);
|
|
860
|
+
if (sameThumb(before, after)) continue;
|
|
861
|
+
// a pixel of slop for the rounded corners' antialiasing, as the
|
|
862
|
+
// blit's own thumb repair takes
|
|
863
|
+
if (before) sink(insetRect(before, -1));
|
|
864
|
+
if (after) sink(insetRect(after, -1));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
821
868
|
/**
|
|
822
869
|
* The bar belongs to the scroller, not to the content under it — the same
|
|
823
870
|
* rule a browser applies. Without this a press on the thumb would be
|
package/src/nodes/text.js
CHANGED
|
@@ -338,6 +338,8 @@ export class TextNode extends Node {
|
|
|
338
338
|
variations: style.variations,
|
|
339
339
|
textRendering: style.textRendering,
|
|
340
340
|
color: style.color,
|
|
341
|
+
letterSpacing: style.letterSpacing,
|
|
342
|
+
features: style.features,
|
|
341
343
|
});
|
|
342
344
|
} else if (child.kind === 'text') {
|
|
343
345
|
child.collectSpans(out);
|
package/src/nodes/textarea.js
CHANGED
|
@@ -137,7 +137,7 @@ export class TextAreaNode extends TextInputNode {
|
|
|
137
137
|
width === undefined || direction !== 'rtl'
|
|
138
138
|
? width
|
|
139
139
|
: Math.max(0, width - CARET_RESERVE * this.scale);
|
|
140
|
-
const key = `${width}|${color}|${shown}|${s.family}|${s.size}|${s.weight}|${s.style}|${direction}|${align}`;
|
|
140
|
+
const key = `${width}|${color}|${shown}|${s.family}|${s.size}|${s.weight}|${s.style}|${direction}|${align}|${s.letterSpacing}|${JSON.stringify(s.features ?? null)}`;
|
|
141
141
|
if (this._valueLayoutKey !== key) {
|
|
142
142
|
this._valueLayoutKey = key;
|
|
143
143
|
this._valueLayoutCache = fonts.layout([{ text: shown, ...s, color }], s, {
|
package/src/nodes/textinput.js
CHANGED
|
@@ -302,7 +302,7 @@ export class TextInputNode extends Node {
|
|
|
302
302
|
const text = this._displayValue();
|
|
303
303
|
const s = this.resolvedTextStyle();
|
|
304
304
|
const direction = this.direction;
|
|
305
|
-
const key = `${text}|${s.family}|${s.size}|${s.weight}|${s.style}|${direction}`;
|
|
305
|
+
const key = `${text}|${s.family}|${s.size}|${s.weight}|${s.style}|${direction}|${s.letterSpacing}|${JSON.stringify(s.features ?? null)}`;
|
|
306
306
|
if (this._valueLayoutKey !== key) {
|
|
307
307
|
this._valueLayoutKey = key;
|
|
308
308
|
this._valueLayoutCache = fonts.layout(text, s, { direction });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A window anchored to a rect in another (#255, #280): where it goes, and
|
|
2
2
|
// following the anchor as it moves.
|
|
3
3
|
|
|
4
|
-
import { anchorOffscreen, anchorRect } from '../../anchor.js';
|
|
4
|
+
import { anchorOffscreen, anchorRect, anchorScreenRect } from '../../anchor.js';
|
|
5
5
|
|
|
6
6
|
/** Anchoring, installed onto `WindowNode.prototype` by window.js. */
|
|
7
7
|
export class WindowAnchoring {
|
|
@@ -32,18 +32,33 @@ export class WindowAnchoring {
|
|
|
32
32
|
* anchor to yet — a ref whose node has not been laid out. */
|
|
33
33
|
_anchorPlacement(size) {
|
|
34
34
|
const anchor = this.props.anchor;
|
|
35
|
-
|
|
36
|
-
if (!node) return null;
|
|
35
|
+
if (!anchor) return null;
|
|
37
36
|
// `anchorRect` is public API and speaks logical pixels on both sides;
|
|
38
37
|
// this caller's `size` came from `_measure` (device) and its result is
|
|
39
38
|
// headed for CreateWindow (device), so both convert here.
|
|
40
39
|
const s = this.scale;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
let rect;
|
|
41
|
+
if (anchor.rect) {
|
|
42
|
+
// A rect on the screen with no node behind it — the tray item a click
|
|
43
|
+
// reported. The popup's own scale and direction stand in for the
|
|
44
|
+
// node's.
|
|
45
|
+
rect = anchorScreenRect(this.app, anchor.rect, {
|
|
46
|
+
...anchor,
|
|
47
|
+
scale: s,
|
|
48
|
+
direction: anchor.direction ?? this.direction,
|
|
49
|
+
width: size.width / s,
|
|
50
|
+
height: size.height / s,
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
const node = this._anchorTarget(anchor.to);
|
|
54
|
+
if (!node) return null;
|
|
55
|
+
rect = anchorRect(node, {
|
|
56
|
+
...anchor,
|
|
57
|
+
alignTo: this._anchorTarget(anchor.alignTo) ?? undefined,
|
|
58
|
+
width: size.width / s,
|
|
59
|
+
height: size.height / s,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
47
62
|
if (!rect || s === 1) return rect;
|
|
48
63
|
return {
|
|
49
64
|
...rect,
|
|
@@ -76,13 +91,7 @@ export class WindowAnchoring {
|
|
|
76
91
|
*/
|
|
77
92
|
_followAnchor(size = this._requestedSize) {
|
|
78
93
|
if (this.destroyed || !this.window || !this.props.anchor) return;
|
|
79
|
-
const
|
|
80
|
-
// A ref that has not attached yet counts as gone, and for the same
|
|
81
|
-
// reason: there is nowhere for the popup to be. Refs attach in the
|
|
82
|
-
// commit phase a popup realizes in, so one written *above* its own
|
|
83
|
-
// trigger in the JSX gets a frame of this — and waiting it out is
|
|
84
|
-
// better than a frame in the corner of the screen.
|
|
85
|
-
const lost = !node || anchorOffscreen(node, this.props.anchor.at);
|
|
94
|
+
const lost = this._anchorGone();
|
|
86
95
|
if (lost !== Boolean(this._anchorLost)) {
|
|
87
96
|
this._anchorLost = lost;
|
|
88
97
|
// The grab goes with it and comes back with it. X releases a pointer
|
|
@@ -93,6 +102,7 @@ export class WindowAnchoring {
|
|
|
93
102
|
// keeps it beside the map on every route back to the screen.
|
|
94
103
|
if (lost) {
|
|
95
104
|
if (this.props.grab) this.window.ungrabPointer?.();
|
|
105
|
+
if (this.props.grabKeyboard) this.window.ungrabKeyboard?.();
|
|
96
106
|
this.window.unmap?.();
|
|
97
107
|
} else {
|
|
98
108
|
this._mapNow();
|
|
@@ -110,14 +120,31 @@ export class WindowAnchoring {
|
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Whether there is nothing for this window to point at: the anchor's node
|
|
125
|
+
* has scrolled out of view, or is a ref that has not attached yet — which
|
|
126
|
+
* counts as gone for the same reason, since there is nowhere for the popup
|
|
127
|
+
* to be. Refs attach in the commit phase a popup realizes in, so one
|
|
128
|
+
* written *above* its own trigger in the JSX gets a frame of this, and
|
|
129
|
+
* waiting it out is better than a frame in the corner of the screen. A
|
|
130
|
+
* rect on the screen is never gone.
|
|
131
|
+
*/
|
|
132
|
+
_anchorGone() {
|
|
133
|
+
const anchor = this.props.anchor;
|
|
134
|
+
if (anchor.rect) return false;
|
|
135
|
+
const node = this._anchorTarget(anchor.to);
|
|
136
|
+
return !node || anchorOffscreen(node, anchor.at);
|
|
137
|
+
}
|
|
138
|
+
|
|
113
139
|
/**
|
|
114
140
|
* Subscribe to whatever can move the anchor. On the **owner's** window,
|
|
115
141
|
* not on this one: what moves is the trigger, and this window's own
|
|
116
|
-
* layout passes say nothing about where it sits on screen.
|
|
142
|
+
* layout passes say nothing about where it sits on screen. A rect on the
|
|
143
|
+
* screen moves only when the app hands a new one, which is a commit.
|
|
117
144
|
*/
|
|
118
145
|
_watchAnchor() {
|
|
119
146
|
this._unwatchAnchor();
|
|
120
|
-
if (!this.props.anchor) return;
|
|
147
|
+
if (!this.props.anchor || this.props.anchor.rect) return;
|
|
121
148
|
// The anchor's own window where the ref has attached, and the window
|
|
122
149
|
// this popup was *written into* otherwise — which is the same one in
|
|
123
150
|
// every case that matters, and is what makes an unattached ref a
|
|
@@ -75,8 +75,8 @@ export class WindowFlush {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* The backend's half of a frame's cost, where it has one: the Cocoa
|
|
78
|
-
* present — the swapchain flip
|
|
79
|
-
*
|
|
78
|
+
* present — the swapchain flip — runs after the flush returns, on the
|
|
79
|
+
* same thread, and is part of what the frame cost
|
|
80
80
|
* (src/cocoa/window.js reports it). An ntk window's present is one
|
|
81
81
|
* request, and reports nothing.
|
|
82
82
|
*/
|
|
@@ -335,11 +335,12 @@ export class WindowFlush {
|
|
|
335
335
|
// after every region: an entry drawn in one damage rect must not be
|
|
336
336
|
// evicted before the next rect of the same frame asks for it
|
|
337
337
|
this._paintCache?.endFrame();
|
|
338
|
-
// The swapchain seam: a backend presenting from
|
|
338
|
+
// The swapchain seam: a backend presenting from a swapchain has to
|
|
339
339
|
// know exactly which pixels each flush touched — several flushes can
|
|
340
340
|
// land between two presents, so reading only the last frame's rects
|
|
341
|
-
// would leave the
|
|
342
|
-
// painted. Feature-detected like presentFrame; null means
|
|
341
|
+
// would leave the next buffer it draws into stale where an earlier
|
|
342
|
+
// flush painted. Feature-detected like presentFrame; null means
|
|
343
|
+
// everything.
|
|
343
344
|
this.window.noteFrameDamage?.(damage ?? null);
|
|
344
345
|
if (frameHook) {
|
|
345
346
|
frameHook({
|
|
@@ -30,15 +30,25 @@ export class PopupNode extends WindowNode {
|
|
|
30
30
|
* from realize looked equivalent until `hidden` existed, and would have
|
|
31
31
|
* left a revealed menu holding no grab: open forever behind the first
|
|
32
32
|
* outside click, with nothing saying why.
|
|
33
|
+
*
|
|
34
|
+
* `grabKeyboard` is the keyboard's half, taken and dropped with the map
|
|
35
|
+
* the same way: keys come to this popup while it is up, whatever holds
|
|
36
|
+
* the focus — what a popover a tray click opened needs, since a menu-bar
|
|
37
|
+
* app has no window of its own for the keys to reach. On Cocoa it is a
|
|
38
|
+
* property of the window rather than a grab, and was decided when the
|
|
39
|
+
* window was made (src/cocoa/window.js); on Wayland a grabbing popup has
|
|
40
|
+
* the keyboard already.
|
|
33
41
|
*/
|
|
34
42
|
_mapNow() {
|
|
35
43
|
if (!super._mapNow()) return false;
|
|
36
44
|
if (this.props.grab) this.window.grabPointer?.({}, () => {});
|
|
45
|
+
if (this.props.grabKeyboard) this.window.grabKeyboard?.({}, () => {});
|
|
37
46
|
return true;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
destroySubtree() {
|
|
41
50
|
if (this.props.grab) this.window?.ungrabPointer?.();
|
|
51
|
+
if (this.props.grabKeyboard) this.window?.ungrabKeyboard?.();
|
|
42
52
|
super.destroySubtree();
|
|
43
53
|
}
|
|
44
54
|
|
package/src/nodes/window/size.js
CHANGED
|
@@ -398,6 +398,18 @@ export class WindowSize {
|
|
|
398
398
|
return { width: props.width, height: props.height, hints };
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// **The floors on hand are the last frame's content.** A node whose
|
|
402
|
+
// content shrank still carries the minimum the old content needed — the
|
|
403
|
+
// automatic minimum size this window wrote into yoga — and a natural-size
|
|
404
|
+
// pass would read it back as content the tree cannot give up, so an
|
|
405
|
+
// `'auto'` window could only ever grow (#586). The stale ones come off
|
|
406
|
+
// first, which is exactly what `_applyContentFloors` does ahead of its
|
|
407
|
+
// own measurement: a node whose extent is stale gets the minimum its
|
|
408
|
+
// style asks for, and a node whose extent still stands keeps the floor it
|
|
409
|
+
// earned. The pass after this one measures and writes them again.
|
|
410
|
+
this._writeFloors('width');
|
|
411
|
+
this._writeFloors('height');
|
|
412
|
+
|
|
401
413
|
const dir = this._rootDirection;
|
|
402
414
|
const measure = () => {
|
|
403
415
|
this._sweepLayoutHosts();
|
|
@@ -525,9 +537,14 @@ export class WindowSize {
|
|
|
525
537
|
);
|
|
526
538
|
if (!tracking && !bounded) return;
|
|
527
539
|
const asked = this._requestedSize;
|
|
528
|
-
const
|
|
529
|
-
this._sendSizeHints(props,
|
|
540
|
+
const measured = this._measure();
|
|
541
|
+
this._sendSizeHints(props, measured.hints);
|
|
530
542
|
if (!tracking) return;
|
|
543
|
+
// The size the backend will really give, compared against the last one
|
|
544
|
+
// for the same reason it is recorded: two natural sizes a third of a
|
|
545
|
+
// point apart are one window size, and asking again every frame for a
|
|
546
|
+
// size that cannot change is a configure per frame.
|
|
547
|
+
const next = this._snapSize(measured);
|
|
531
548
|
if (asked && next.width === asked.width && next.height === asked.height) {
|
|
532
549
|
return;
|
|
533
550
|
}
|
|
@@ -553,6 +570,27 @@ export class WindowSize {
|
|
|
553
570
|
this._followAnchor({ width: next.width, height: next.height });
|
|
554
571
|
}
|
|
555
572
|
|
|
573
|
+
/**
|
|
574
|
+
* The size the backend will really take for one this window asks for.
|
|
575
|
+
*
|
|
576
|
+
* A window is not always free to be exactly the size its content wants:
|
|
577
|
+
* Cocoa puts one on the **point** grid, so a natural height of 201 device
|
|
578
|
+
* pixels at scale 2 is a window 202 tall. Every record of
|
|
579
|
+
* `_requestedSize` goes through here, because that record is what the
|
|
580
|
+
* resize echo is compared against — and an echo that does not match it
|
|
581
|
+
* reads as the user or the window manager taking the size over, which
|
|
582
|
+
* ends an `'auto'` window's tracking for good (#586). A backend that
|
|
583
|
+
* takes any size, X11's, answers with the size it was handed.
|
|
584
|
+
*/
|
|
585
|
+
_snapSize(size) {
|
|
586
|
+
return (
|
|
587
|
+
this.window?.snapSize?.(size.width, size.height) ?? {
|
|
588
|
+
width: size.width,
|
|
589
|
+
height: size.height,
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
556
594
|
/** One layout pass at the window's size, with the content floors it
|
|
557
595
|
* needs: fresh ones when something changed them, the ones in hand during
|
|
558
596
|
* a live resize, none when nothing moved them. */
|