react-x11 2.38.0 → 2.40.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/package.json +3 -3
- package/src/acceleratorhooks.js +24 -8
- package/src/accelerators.js +26 -9
- package/src/backend/context2d.js +292 -35
- package/src/backend/filter.js +285 -0
- package/src/cocoa/app.js +7 -0
- package/src/cocoa/globalmenu.js +32 -1
- package/src/cocoa/panehost.js +117 -1
- package/src/cocoa/sprites.js +95 -18
- package/src/components/Menu.js +57 -11
- package/src/components/PasswordInput.js +11 -6
- package/src/editmenu.js +9 -2
- package/src/events.js +24 -0
- package/src/frame/index.js +1 -1
- package/src/globalmenu.js +21 -3
- package/src/index.d.ts +4 -1
- package/src/keysyms.d.ts +30 -0
- package/src/keysyms.js +29 -0
- package/src/menuitem.js +63 -17
- package/src/node.d.ts +44 -3
- package/src/nodes/editmenupopup.js +21 -2
- package/src/nodes/sprites.js +6 -3
- package/src/nodes/textarea.js +3 -1
- package/src/nodes/textinput.js +13 -3
- package/src/textselection.js +7 -7
- package/src/types/components.d.ts +8 -2
- package/src/types/elements.d.ts +1 -1
- package/src/types/events.d.ts +21 -3
- package/src/types/nodes.d.ts +6 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `filter` on the native 2d context: canvas's own property, a CSS
|
|
2
|
+
// `<filter-value-list>` (Filter Effects 1), and what this context makes of
|
|
3
|
+
// it — the colour functions, each a matrix over straight RGB with an
|
|
4
|
+
// offset that leaves alpha as it is, and `opacity()`, which scales alpha.
|
|
5
|
+
//
|
|
6
|
+
// They are the functions the context can apply to everything it draws.
|
|
7
|
+
// Each leaves alpha alone, so on premultiplied colour each is a linear map
|
|
8
|
+
// that commutes with source-over: a fill, a stroke, a gradient's stops, a
|
|
9
|
+
// glyph run's ink and a symbol's colour are filtered by filtering the colour
|
|
10
|
+
// they are drawn in, exactly. An image and a surface are filtered pixel by
|
|
11
|
+
// pixel, and a text layout that carries colours of its own on a surface of
|
|
12
|
+
// its own (src/backend/context2d.js). `blur()`, `drop-shadow()` and a
|
|
13
|
+
// `url()` reach past what is drawn, which a colour cannot say; this context
|
|
14
|
+
// does not apply them, so a list with one in it does not stick, and a
|
|
15
|
+
// caller knows from reading the property back.
|
|
16
|
+
|
|
17
|
+
/** A function's amount where none is written: all of the effect for the
|
|
18
|
+
* ones whose argument is how much of it to take, and none for the others
|
|
19
|
+
* (Filter Effects 1, 13). */
|
|
20
|
+
const AMOUNTS = {
|
|
21
|
+
grayscale: 1,
|
|
22
|
+
sepia: 1,
|
|
23
|
+
invert: 1,
|
|
24
|
+
opacity: 1,
|
|
25
|
+
saturate: 1,
|
|
26
|
+
brightness: 1,
|
|
27
|
+
contrast: 1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The functions this context applies; any other leaves a list unapplied. */
|
|
31
|
+
const COLOUR = new Set([...Object.keys(AMOUNTS), 'hue-rotate']);
|
|
32
|
+
|
|
33
|
+
const ANGLE_UNITS = {
|
|
34
|
+
deg: 1,
|
|
35
|
+
grad: 0.9,
|
|
36
|
+
rad: 180 / Math.PI,
|
|
37
|
+
turn: 360,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A filter list as this context applies it: `{ matrices, alpha }`, the
|
|
42
|
+
* colour matrices in the order written, each over straight RGB in 0–1 with
|
|
43
|
+
* an offset — none for a function that changes no colour — and what alpha
|
|
44
|
+
* is multiplied by. Null for `none`; undefined for what is no filter list,
|
|
45
|
+
* or one with a function this context does not apply.
|
|
46
|
+
*/
|
|
47
|
+
export function parseCanvasFilter(value) {
|
|
48
|
+
const text = String(value).trim();
|
|
49
|
+
if (text.toLowerCase() === 'none') return null;
|
|
50
|
+
const matrices = [];
|
|
51
|
+
let alpha = 1;
|
|
52
|
+
let rest = text;
|
|
53
|
+
let seen = 0;
|
|
54
|
+
while (rest.length) {
|
|
55
|
+
const m = /^([a-z-]+)\(([^()]*)\)\s*/i.exec(rest);
|
|
56
|
+
if (!m) return undefined;
|
|
57
|
+
rest = rest.slice(m[0].length);
|
|
58
|
+
seen += 1;
|
|
59
|
+
const name = m[1].toLowerCase();
|
|
60
|
+
if (!COLOUR.has(name)) return undefined;
|
|
61
|
+
const arg = m[2].trim();
|
|
62
|
+
if (name === 'hue-rotate') {
|
|
63
|
+
const angle = arg ? angleOf(arg) : 0;
|
|
64
|
+
if (angle === null) return undefined;
|
|
65
|
+
const matrix = hueRotate(angle);
|
|
66
|
+
if (matrix) matrices.push(matrix);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const amount = arg ? amountOf(arg) : AMOUNTS[name];
|
|
70
|
+
if (amount === null || amount < 0) return undefined;
|
|
71
|
+
if (name === 'opacity') {
|
|
72
|
+
alpha *= Math.min(1, amount);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const matrix = matrixOf(name, amount);
|
|
76
|
+
if (matrix) matrices.push(matrix);
|
|
77
|
+
}
|
|
78
|
+
if (!seen) return undefined;
|
|
79
|
+
return { matrices, alpha };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether a parsed filter changes anything drawn. */
|
|
83
|
+
export function filters(filter) {
|
|
84
|
+
return !!filter && (filter.matrices.length > 0 || filter.alpha < 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A number or a percentage of one. */
|
|
88
|
+
function amountOf(arg) {
|
|
89
|
+
const m = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(%?)$/i.exec(arg);
|
|
90
|
+
if (!m) return null;
|
|
91
|
+
const n = Number(m[1]);
|
|
92
|
+
if (!Number.isFinite(n)) return null;
|
|
93
|
+
return m[2] ? n / 100 : n;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** An angle in degrees; a bare 0 is one. */
|
|
97
|
+
function angleOf(arg) {
|
|
98
|
+
const m = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z]*)$/i.exec(arg);
|
|
99
|
+
if (!m) return null;
|
|
100
|
+
const n = Number(m[1]);
|
|
101
|
+
const unit = m[2].toLowerCase();
|
|
102
|
+
if (!unit) return n === 0 ? 0 : null;
|
|
103
|
+
const scale = ANGLE_UNITS[unit];
|
|
104
|
+
return scale === undefined ? null : n * scale;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const unit = (a) => (a < 0 ? 0 : a > 1 ? 1 : a);
|
|
108
|
+
|
|
109
|
+
/** Three rows of red, green and blue weights and an offset, 0–1. */
|
|
110
|
+
function rows(r, g, b) {
|
|
111
|
+
return Float64Array.of(...r, ...g, ...b);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `feComponentTransfer`'s `linear`: each channel times a slope, plus an
|
|
115
|
+
* intercept. */
|
|
116
|
+
function linear(slope, intercept) {
|
|
117
|
+
return Float64Array.of(
|
|
118
|
+
slope,
|
|
119
|
+
0,
|
|
120
|
+
0,
|
|
121
|
+
intercept,
|
|
122
|
+
0,
|
|
123
|
+
slope,
|
|
124
|
+
0,
|
|
125
|
+
intercept,
|
|
126
|
+
0,
|
|
127
|
+
0,
|
|
128
|
+
slope,
|
|
129
|
+
intercept,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One function's matrix (Filter Effects 1, 13); null where it changes no
|
|
134
|
+
* colour. */
|
|
135
|
+
function matrixOf(name, amount) {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case 'grayscale': {
|
|
138
|
+
const a = unit(amount);
|
|
139
|
+
if (a === 0) return null;
|
|
140
|
+
const k = 1 - a;
|
|
141
|
+
return rows(
|
|
142
|
+
[0.2126 + 0.7874 * k, 0.7152 - 0.7152 * k, 0.0722 - 0.0722 * k, 0],
|
|
143
|
+
[0.2126 - 0.2126 * k, 0.7152 + 0.2848 * k, 0.0722 - 0.0722 * k, 0],
|
|
144
|
+
[0.2126 - 0.2126 * k, 0.7152 - 0.7152 * k, 0.0722 + 0.9278 * k, 0],
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
case 'sepia': {
|
|
148
|
+
const a = unit(amount);
|
|
149
|
+
if (a === 0) return null;
|
|
150
|
+
const k = 1 - a;
|
|
151
|
+
return rows(
|
|
152
|
+
[0.393 + 0.607 * k, 0.769 - 0.769 * k, 0.189 - 0.189 * k, 0],
|
|
153
|
+
[0.349 - 0.349 * k, 0.686 + 0.314 * k, 0.168 - 0.168 * k, 0],
|
|
154
|
+
[0.272 - 0.272 * k, 0.534 - 0.534 * k, 0.131 + 0.869 * k, 0],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
case 'saturate': {
|
|
158
|
+
const s = amount;
|
|
159
|
+
if (s === 1) return null;
|
|
160
|
+
return rows(
|
|
161
|
+
[0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
|
|
162
|
+
[0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
|
|
163
|
+
[0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0],
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
case 'brightness':
|
|
167
|
+
return amount === 1 ? null : linear(amount, 0);
|
|
168
|
+
case 'contrast':
|
|
169
|
+
return amount === 1 ? null : linear(amount, 0.5 - 0.5 * amount);
|
|
170
|
+
case 'invert': {
|
|
171
|
+
const a = unit(amount);
|
|
172
|
+
return a === 0 ? null : linear(1 - 2 * a, a);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hueRotate(degrees) {
|
|
179
|
+
if (degrees % 360 === 0) return null;
|
|
180
|
+
const t = (degrees * Math.PI) / 180;
|
|
181
|
+
const c = Math.cos(t);
|
|
182
|
+
const s = Math.sin(t);
|
|
183
|
+
return rows(
|
|
184
|
+
[
|
|
185
|
+
0.213 + c * 0.787 - s * 0.213,
|
|
186
|
+
0.715 - c * 0.715 - s * 0.715,
|
|
187
|
+
0.072 - c * 0.072 + s * 0.928,
|
|
188
|
+
0,
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
0.213 - c * 0.213 + s * 0.143,
|
|
192
|
+
0.715 + c * 0.285 + s * 0.14,
|
|
193
|
+
0.072 - c * 0.072 - s * 0.283,
|
|
194
|
+
0,
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
0.213 - c * 0.213 - s * 0.787,
|
|
198
|
+
0.715 - c * 0.715 + s * 0.715,
|
|
199
|
+
0.072 + c * 0.928 + s * 0.072,
|
|
200
|
+
0,
|
|
201
|
+
],
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A straight `[r, g, b, a]`, 0–1, through the filter: each function's
|
|
206
|
+
* result clamped before the next, as each one's is. */
|
|
207
|
+
export function filterColour([r, g, b, a], filter) {
|
|
208
|
+
for (const m of filter.matrices) {
|
|
209
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3];
|
|
210
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7];
|
|
211
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11];
|
|
212
|
+
r = unit(r1);
|
|
213
|
+
g = unit(g1);
|
|
214
|
+
b = unit(b1);
|
|
215
|
+
}
|
|
216
|
+
return [r, g, b, a * filter.alpha];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A gradient's stops, flat as `[offset, r, g, b, a, …]`, through the
|
|
220
|
+
* filter: interpolating straight colours commutes with a matrix over them,
|
|
221
|
+
* but for what a clamp cuts off between two stops. */
|
|
222
|
+
export function filterStops(flat, filter) {
|
|
223
|
+
const out = flat.slice();
|
|
224
|
+
for (let i = 0; i + 4 < out.length; i += 5) {
|
|
225
|
+
const [r, g, b, a] = filterColour(
|
|
226
|
+
[out[i + 1], out[i + 2], out[i + 3], out[i + 4]],
|
|
227
|
+
filter,
|
|
228
|
+
);
|
|
229
|
+
out[i + 1] = r;
|
|
230
|
+
out[i + 2] = g;
|
|
231
|
+
out[i + 3] = b;
|
|
232
|
+
out[i + 4] = a;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Straight RGBA pixels, 0–255, through the filter, `src` into `dst` — the
|
|
239
|
+
* two may be one. A pixel with no alpha keeps none: its colour is no
|
|
240
|
+
* colour.
|
|
241
|
+
*/
|
|
242
|
+
export function filterPixels(src, dst, filter) {
|
|
243
|
+
const { matrices } = filter;
|
|
244
|
+
const fade = filter.alpha;
|
|
245
|
+
const one = matrices.length === 1 ? matrices[0] : null;
|
|
246
|
+
for (let i = 0; i < src.length; i += 4) {
|
|
247
|
+
const a = src[i + 3];
|
|
248
|
+
if (a === 0) {
|
|
249
|
+
dst[i] = 0;
|
|
250
|
+
dst[i + 1] = 0;
|
|
251
|
+
dst[i + 2] = 0;
|
|
252
|
+
dst[i + 3] = 0;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let r = src[i];
|
|
256
|
+
let g = src[i + 1];
|
|
257
|
+
let b = src[i + 2];
|
|
258
|
+
if (one) {
|
|
259
|
+
const m = one;
|
|
260
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3] * 255;
|
|
261
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7] * 255;
|
|
262
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11] * 255;
|
|
263
|
+
r = r1;
|
|
264
|
+
g = g1;
|
|
265
|
+
b = b1;
|
|
266
|
+
} else {
|
|
267
|
+
for (const m of matrices) {
|
|
268
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3] * 255;
|
|
269
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7] * 255;
|
|
270
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11] * 255;
|
|
271
|
+
r = r1 < 0 ? 0 : r1 > 255 ? 255 : r1;
|
|
272
|
+
g = g1 < 0 ? 0 : g1 > 255 ? 255 : g1;
|
|
273
|
+
b = b1 < 0 ? 0 : b1 > 255 ? 255 : b1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
dst[i] = clampByte(r);
|
|
277
|
+
dst[i + 1] = clampByte(g);
|
|
278
|
+
dst[i + 2] = clampByte(b);
|
|
279
|
+
dst[i + 3] = fade === 1 ? a : clampByte(a * fade);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function clampByte(v) {
|
|
284
|
+
return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);
|
|
285
|
+
}
|
package/src/cocoa/app.js
CHANGED
|
@@ -286,6 +286,13 @@ export class CocoaApp {
|
|
|
286
286
|
// X11's, where every toolkit hovers in a window without the keyboard.
|
|
287
287
|
this.hoverNeedsActiveWindow = true;
|
|
288
288
|
|
|
289
|
+
// Shortcuts are pressed with ⌘ here (src/keysyms.js `primaryModifier`):
|
|
290
|
+
// the text controls' Undo/Cut/Copy/Paste/Select All answer ⌘ rather
|
|
291
|
+
// than ⌃, and the edit menu prints them the way a Mac menu does. The
|
|
292
|
+
// menu bar already read a chord's `Control` as ⌘ (cocoa/globalmenu.js);
|
|
293
|
+
// this is the same rule for everything else that reads one.
|
|
294
|
+
this.primaryModifier = 'Super';
|
|
295
|
+
|
|
289
296
|
// AppKit-rendered control bezels. Its *presence* is the capability:
|
|
290
297
|
// `useSupports('nativeControls')` and the widget set's `controls:
|
|
291
298
|
// 'auto'` policy both test for this property, so a backend without it
|
package/src/cocoa/globalmenu.js
CHANGED
|
@@ -35,6 +35,9 @@ const FLAG_COMMAND = 1 << 20;
|
|
|
35
35
|
* user presses. Multi-chord sequences and non-character keys have no
|
|
36
36
|
* NSMenu spelling and are dropped (the shortcut still works — the app's
|
|
37
37
|
* own key handling is untouched; only the menu's hint goes unshown).
|
|
38
|
+
*
|
|
39
|
+
* `named` is the modifiers the chord itself asked for, before the ⌘ a bare
|
|
40
|
+
* key is given so that NSMenu has something to show.
|
|
38
41
|
*/
|
|
39
42
|
function keyEquivalent(shortcut) {
|
|
40
43
|
const chord = Array.isArray(shortcut) ? shortcut[0] : null;
|
|
@@ -48,7 +51,26 @@ function keyEquivalent(shortcut) {
|
|
|
48
51
|
else key = part;
|
|
49
52
|
}
|
|
50
53
|
if (typeof key !== 'string' || [...key].length !== 1) return null;
|
|
51
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
key: key.toLowerCase(),
|
|
56
|
+
modifiers: modifiers || FLAG_COMMAND,
|
|
57
|
+
named: modifiers,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Does the menu bar answer this `shortcut` itself, on the chord the app
|
|
63
|
+
* wrote? The ones it holds as a key equivalent that carries ⌘ because the
|
|
64
|
+
* chord said `Control` or `Super`: AppKit offers a ⌘ key to the main menu's
|
|
65
|
+
* `performKeyEquivalent:` inside `[NSApp sendEvent:]`, so the item fires
|
|
66
|
+
* with no help from us. A bare key padded out to ⌘ is not one — the chord
|
|
67
|
+
* names the bare key, and the window still answers that.
|
|
68
|
+
*
|
|
69
|
+
* The bridge shows JS every key before AppKit sees it, so a `MenuBar` that
|
|
70
|
+
* matched these too would run the item twice (`useMenuAccelerators`).
|
|
71
|
+
*/
|
|
72
|
+
export function answersShortcut(shortcut) {
|
|
73
|
+
return Boolean(keyEquivalent(shortcut)?.named & FLAG_COMMAND);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/**
|
|
@@ -117,6 +139,15 @@ export class CocoaGlobalMenuExport {
|
|
|
117
139
|
if (this.app._activeGlobalMenu === this) this._install();
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The delegate's half of the key: the shortcuts the menu bar answers on
|
|
144
|
+
* its own, which the window must then leave alone (`answersShortcut`).
|
|
145
|
+
* The D-Bus export has no such method, because a panel delivers no keys.
|
|
146
|
+
*/
|
|
147
|
+
answersShortcut(shortcut) {
|
|
148
|
+
return answersShortcut(shortcut);
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
_install() {
|
|
121
152
|
this.app._native.setMainMenu(this._spec());
|
|
122
153
|
}
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -12,9 +12,25 @@
|
|
|
12
12
|
// And the players its `<video src>`s ask for, which a process with no
|
|
13
13
|
// AppKit would never hear from: each is a player here, its events and its
|
|
14
14
|
// frames sent back (`HostedPlayer`, src/cocoa/paneplayer.js).
|
|
15
|
+
import { now } from '../nodes/animation.js';
|
|
15
16
|
import { HostedPlayer } from './paneplayer.js';
|
|
16
17
|
import { withoutActions } from './quiet.js';
|
|
17
18
|
|
|
19
|
+
/** The live-resize handshake's budget when the app names none
|
|
20
|
+
* (`RESIZE_WAIT_MS`, src/cocoa/app.js). */
|
|
21
|
+
const RESIZE_WAIT_MS = 50;
|
|
22
|
+
|
|
23
|
+
/** How many sizes asked of the pane are remembered until a frame of one
|
|
24
|
+
* answers them (`_asked`): a drag asks one a frame. */
|
|
25
|
+
const ASKED_KEPT = 16;
|
|
26
|
+
|
|
27
|
+
/** How long after the last size asked a new one starts a gesture of its
|
|
28
|
+
* own, which finds the pane keeping up until it shows otherwise: a pane
|
|
29
|
+
* whose layout at a width it was not dragged to took long — a long page
|
|
30
|
+
* laid out whole, as the first step of a drag is — keeps up with the
|
|
31
|
+
* drag itself. */
|
|
32
|
+
const GESTURE_GAP_MS = 500;
|
|
33
|
+
|
|
18
34
|
/** One shared surface on glass: a sublayer of a window's root layer, its
|
|
19
35
|
* contents the IOSurface the pane last presented into it. */
|
|
20
36
|
class PaneLayer {
|
|
@@ -29,6 +45,82 @@ class PaneLayer {
|
|
|
29
45
|
});
|
|
30
46
|
this.destroyed = false;
|
|
31
47
|
this._rect = null;
|
|
48
|
+
// How the frame the layer holds sits in it while one of the size the
|
|
49
|
+
// layer was just given is on its way (`_gravityNow`): the gravity set,
|
|
50
|
+
// Core Animation's own until one is, the sizes asked of the pane with
|
|
51
|
+
// when each was asked, and whether the pane has fallen behind them.
|
|
52
|
+
this._gravity = 'resize';
|
|
53
|
+
this._contentsScale = null;
|
|
54
|
+
this._asked = [];
|
|
55
|
+
this._askedAt = null;
|
|
56
|
+
this._behind = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The gravity a pane's last frame sits in its layer by, until a frame of
|
|
61
|
+
* the layer's new size lands. The host gives the layer its new size the
|
|
62
|
+
* moment the window lays out, and the pane's frame of that size comes a
|
|
63
|
+
* pane frame later: Core Animation's default, `resize`, stretches the
|
|
64
|
+
* frame it has to the new bounds meanwhile, and a page's left column was
|
|
65
|
+
* drawn scaled a little at every step of a window dragged wider, and then
|
|
66
|
+
* at its size again. A pane that keeps up has its frame anchored at the
|
|
67
|
+
* top left instead, at its size, what it does not cover yet showing the
|
|
68
|
+
* window under it for a pane frame; one that falls behind the budget is
|
|
69
|
+
* stretched, which is the better picture for the long wait — a pane
|
|
70
|
+
* repainting at three frames a second shows a squeezed page, not a page
|
|
71
|
+
* and a growing strip. The budget is the window's live-resize handshake's
|
|
72
|
+
* (`createRoot({ cocoa: { resizeWait } })`), how long a frame of a new
|
|
73
|
+
* size may take before the old one is shown in its place: 0 stretches
|
|
74
|
+
* always, as before. A bridge that predates `contentsGravity` ignores it,
|
|
75
|
+
* and stretches.
|
|
76
|
+
*/
|
|
77
|
+
_gravityNow() {
|
|
78
|
+
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
79
|
+
if (!(budget > 0)) return 'resize';
|
|
80
|
+
// a size asked longer ago than the budget, and no frame of it yet
|
|
81
|
+
const oldest = this._asked[0];
|
|
82
|
+
if (oldest && now() - oldest.at > budget) this._behind = true;
|
|
83
|
+
return this._behind ? 'resize' : 'topLeft';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The props that set the gravity, where it changed. */
|
|
87
|
+
_gravityProps() {
|
|
88
|
+
const gravity = this._gravityNow();
|
|
89
|
+
if (gravity === this._gravity) return null;
|
|
90
|
+
this._gravity = gravity;
|
|
91
|
+
// a frame anchored and larger than the layer would hang out of it
|
|
92
|
+
return { contentsGravity: gravity, masksToBounds: gravity !== 'resize' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The props that say how many of the pane's pixels make a point of the
|
|
97
|
+
* layer, where that changed: the window's scale, which the pane draws
|
|
98
|
+
* at. A frame stretched to the bounds never asked, and one anchored at
|
|
99
|
+
* its size is shown at it — pixels over `contentsScale` points, so at
|
|
100
|
+
* the default of 1 a page at 2x came out twice its size.
|
|
101
|
+
*/
|
|
102
|
+
_scaleProps() {
|
|
103
|
+
const scale = this.wnd.scale ?? 1;
|
|
104
|
+
if (scale === this._contentsScale) return null;
|
|
105
|
+
this._contentsScale = scale;
|
|
106
|
+
return { contentsScale: scale };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A frame of `width` by `height` device px landed: what it answers of
|
|
110
|
+
* the sizes asked, and how long it took. */
|
|
111
|
+
_answered(width, height) {
|
|
112
|
+
const asked = this._asked;
|
|
113
|
+
// the pane rounds the logical size it was sent back to device pixels
|
|
114
|
+
const slack = Math.max(2, this.wnd.scale ?? 1);
|
|
115
|
+
const at = asked.findIndex(
|
|
116
|
+
(a) =>
|
|
117
|
+
Math.abs(a.width - width) <= slack &&
|
|
118
|
+
Math.abs(a.height - height) <= slack,
|
|
119
|
+
);
|
|
120
|
+
if (at < 0) return;
|
|
121
|
+
this._behind =
|
|
122
|
+
now() - asked[at].at > (this.app._resizeWait ?? RESIZE_WAIT_MS);
|
|
123
|
+
asked.splice(0, at + 1);
|
|
32
124
|
}
|
|
33
125
|
|
|
34
126
|
/** Geometry in device px, the node's abs — points at the layer. */
|
|
@@ -46,6 +138,17 @@ class PaneLayer {
|
|
|
46
138
|
return;
|
|
47
139
|
}
|
|
48
140
|
this._rect = { ...rect };
|
|
141
|
+
// a size the pane is asked for, answered by the first frame of it
|
|
142
|
+
if (prev && (prev.width !== rect.width || prev.height !== rect.height)) {
|
|
143
|
+
const at = now();
|
|
144
|
+
if (at - (this._askedAt ?? -Infinity) >= GESTURE_GAP_MS) {
|
|
145
|
+
this._asked = [];
|
|
146
|
+
this._behind = false;
|
|
147
|
+
}
|
|
148
|
+
this._askedAt = at;
|
|
149
|
+
if (this._asked.length === ASKED_KEPT) this._asked.shift();
|
|
150
|
+
this._asked.push({ width: rect.width, height: rect.height, at });
|
|
151
|
+
}
|
|
49
152
|
// Actions off: the layer is ours, not a presenter's, so no frame's
|
|
50
153
|
// transaction covers it, and a bare set tweens the pane into place at
|
|
51
154
|
// mount and after every resize — the trap CocoaGLArea._setLayerProps
|
|
@@ -57,6 +160,8 @@ class PaneLayer {
|
|
|
57
160
|
frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
|
|
58
161
|
zPosition: 1e7,
|
|
59
162
|
hidden: false,
|
|
163
|
+
...this._scaleProps(),
|
|
164
|
+
...this._gravityProps(),
|
|
60
165
|
});
|
|
61
166
|
} finally {
|
|
62
167
|
native.txCommit();
|
|
@@ -78,7 +183,7 @@ class PaneLayer {
|
|
|
78
183
|
* already holds a reference to. Nothing else here throws; any other
|
|
79
184
|
* error is the bug it says it is.
|
|
80
185
|
*/
|
|
81
|
-
present(iosurfaceId) {
|
|
186
|
+
present(iosurfaceId, size = null) {
|
|
82
187
|
if (this.destroyed) return false;
|
|
83
188
|
try {
|
|
84
189
|
this._native.setLayerContentsIOSurface(this.layer, iosurfaceId);
|
|
@@ -86,6 +191,17 @@ class PaneLayer {
|
|
|
86
191
|
if (!/IOSurfaceLookup/.test(err?.message ?? '')) throw err;
|
|
87
192
|
return false;
|
|
88
193
|
}
|
|
194
|
+
// how long the pane took to a frame of a size it was asked for, which
|
|
195
|
+
// is what the gravity follows (`_gravityNow`)
|
|
196
|
+
if (size) {
|
|
197
|
+
this._answered(size.width, size.height);
|
|
198
|
+
const gravity = this._gravityProps();
|
|
199
|
+
if (gravity) {
|
|
200
|
+
withoutActions(this._native, () =>
|
|
201
|
+
this._native.setLayerProps(this.layer, gravity),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
89
205
|
return true;
|
|
90
206
|
}
|
|
91
207
|
|