react-x11 2.40.0 → 2.41.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/package.json +4 -4
- package/src/cocoa/app.js +19 -8
- package/src/cocoa/keymap.js +69 -12
- package/src/cocoa/panehost.js +92 -19
- package/src/cocoa/paneplayer.js +101 -24
- package/src/cocoa/panewindow.js +325 -44
- package/src/cocoa/window.js +9 -1
- package/src/events.js +12 -0
- package/src/frame/index.js +63 -17
- package/src/nodes/textarea.js +85 -15
- package/src/nodes/textinput.js +14 -3
- package/src/nodes/textkeys.js +322 -0
- package/src/offscreen.js +8 -7
- package/src/types/events.d.ts +6 -2
- package/src/win32/fonts.js +635 -47
- package/src/win32/panewindow.js +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.41.1",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -104,14 +104,14 @@
|
|
|
104
104
|
"ical.js": "^2.2.1",
|
|
105
105
|
"image-in-browser": "^3.6.0",
|
|
106
106
|
"linebreak": "^1.1.0",
|
|
107
|
-
"ntk": "^8.
|
|
107
|
+
"ntk": "^8.23.0",
|
|
108
108
|
"react-reconciler": "^0.34.0",
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@windowkit/appkit": "^0.
|
|
112
|
+
"@windowkit/appkit": "^0.25.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
|
-
"@windowkit/win32": "^0.0.
|
|
114
|
+
"@windowkit/win32": "^0.0.9",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
|
116
116
|
"x11-dri": "^0.9.0"
|
|
117
117
|
},
|
package/src/cocoa/app.js
CHANGED
|
@@ -290,7 +290,9 @@ export class CocoaApp {
|
|
|
290
290
|
// the text controls' Undo/Cut/Copy/Paste/Select All answer ⌘ rather
|
|
291
291
|
// than ⌃, and the edit menu prints them the way a Mac menu does. The
|
|
292
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.
|
|
293
|
+
// this is the same rule for everything else that reads one. Text is
|
|
294
|
+
// edited the Mac way with it too: ⌥ moves by word, ⌘ to the ends of a
|
|
295
|
+
// line and the document, ⌃ is Emacs's (src/nodes/textkeys.js).
|
|
294
296
|
this.primaryModifier = 'Super';
|
|
295
297
|
|
|
296
298
|
// AppKit-rendered control bezels. Its *presence* is the capability:
|
|
@@ -934,8 +936,9 @@ export class CocoaApp {
|
|
|
934
936
|
|
|
935
937
|
/**
|
|
936
938
|
* The pane's end of the frame channel (childmain hands it over,
|
|
937
|
-
* feature-detected so the X11 pane path never notices): geometry
|
|
938
|
-
*
|
|
939
|
+
* feature-detected so the X11 pane path never notices): geometry, input
|
|
940
|
+
* and the host's word that it has looked a present up come in,
|
|
941
|
+
* pane-present and pane-cursor go out. `players` is the
|
|
939
942
|
* host's word that it plays the pane's `<video src>`s, which makes
|
|
940
943
|
* `createPlayer` here — before the pane's first render, which asks.
|
|
941
944
|
*/
|
|
@@ -963,7 +966,12 @@ export class CocoaApp {
|
|
|
963
966
|
}
|
|
964
967
|
const pane = this._paneWindow ?? [...this._windows.values()][0];
|
|
965
968
|
if (!pane) return;
|
|
966
|
-
if (msg?.type === 'pane-
|
|
969
|
+
if (msg?.type === 'pane-shown') {
|
|
970
|
+
// the host has looked up this window's presents to `seq`, so the
|
|
971
|
+
// buffers they named may go (`CocoaPaneWindow._releaseRing`)
|
|
972
|
+
const wnd = msg.window != null ? this._windows.get(msg.window) : pane;
|
|
973
|
+
wnd?._shown?.(msg.seq);
|
|
974
|
+
} else if (msg?.type === 'pane-rect') {
|
|
967
975
|
pane.setPaneSize(msg.width, msg.height, msg.scale, msg.screen);
|
|
968
976
|
this._flushPaneOutbox();
|
|
969
977
|
this._afterInput();
|
|
@@ -1210,11 +1218,12 @@ export class CocoaApp {
|
|
|
1210
1218
|
const surfaces = [];
|
|
1211
1219
|
for (const entry of queue) {
|
|
1212
1220
|
// A window whose last flip has not reached its layer yet (threaded
|
|
1213
|
-
// mode's fence, `CocoaWindow.
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1221
|
+
// mode's fence, `CocoaWindow.frameHeld`), or a pane with no buffer the
|
|
1222
|
+
// host is done with (`CocoaPaneWindow.frameHeld`), waits before its
|
|
1223
|
+
// clock is asked, or the clock would count a frame that did not run.
|
|
1224
|
+
// The release is an event — a batch, a `pane-shown` — and it ticks
|
|
1216
1225
|
// again.
|
|
1217
|
-
if (entry.wnd?.
|
|
1226
|
+
if (entry.wnd?.frameHeld?.()) {
|
|
1218
1227
|
this._rafQueue.push(entry);
|
|
1219
1228
|
continue;
|
|
1220
1229
|
}
|
|
@@ -1560,6 +1569,8 @@ export class CocoaApp {
|
|
|
1560
1569
|
keysym: decoded.keysym,
|
|
1561
1570
|
baseKeysym: decoded.baseKeysym,
|
|
1562
1571
|
codepoint: decoded.codepoint,
|
|
1572
|
+
// more than one character, which no code point carries (keymap.js)
|
|
1573
|
+
text: decoded.text,
|
|
1563
1574
|
buttons: modifierMask(ev),
|
|
1564
1575
|
group: 0,
|
|
1565
1576
|
time: ev.time,
|
package/src/cocoa/keymap.js
CHANGED
|
@@ -5,8 +5,39 @@
|
|
|
5
5
|
// have fixed keysyms looked up here by kVK_* virtual key code. NSEvent's
|
|
6
6
|
// three character views map onto the three keysym roles events.js reads:
|
|
7
7
|
//
|
|
8
|
-
// charsBase (modifiers stripped)
|
|
9
|
-
//
|
|
8
|
+
// charsBase (modifiers stripped) -> baseKeysym — what chords match
|
|
9
|
+
// chars (every modifier applied) -> keysym / codepoint — what was
|
|
10
|
+
// typed, unless ⌘ or ⌃ is held
|
|
11
|
+
// charsShifted (only Shift applied) -> keysym / codepoint of a ⌘ or
|
|
12
|
+
// ⌃ chord, which types nothing
|
|
13
|
+
//
|
|
14
|
+
// **⌥ types on a Mac.** It is the layout's third and fourth level: ⌥S is ß on
|
|
15
|
+
// a US keyboard, and on a German one ⌥L is @, ⌥5 [ and ⌥8 { — the only way
|
|
16
|
+
// to type them. `charsShifted` is `charactersIgnoringModifiers`, which drops
|
|
17
|
+
// ⌥ along with everything but Shift, so reading the typed character there
|
|
18
|
+
// made ⌥S type `s` and left a German keyboard no @ at all. `chars` is
|
|
19
|
+
// `characters`, the layout's own answer with ⌥, Shift and Caps Lock applied —
|
|
20
|
+
// Caps Lock is the other thing `charsShifted` leaves out, so a field typed
|
|
21
|
+
// lower case with it on. It is also exactly the DOM's `key` on macOS: ⌥S is
|
|
22
|
+
// `ß`, ⌘S is `s`.
|
|
23
|
+
//
|
|
24
|
+
// There is no input method behind this. The bridge's view swallows
|
|
25
|
+
// `keyDown:` without `interpretKeyEvents:` and is no `NSTextInputClient`
|
|
26
|
+
// (docs/macos.md, "IME"), so these fields are all the text there is. Two
|
|
27
|
+
// consequences, both handled below:
|
|
28
|
+
//
|
|
29
|
+
// - **A dead key types nothing.** ⌥E on a US layout leaves `chars` empty, and
|
|
30
|
+
// the key it is waiting for decides what the accent becomes. Typing the
|
|
31
|
+
// key's own letter, which is what reading `charsShifted` did, put an `e` in
|
|
32
|
+
// the field. Whether the next key's `chars` carries the composed `é` is the
|
|
33
|
+
// window server's business — an NSEvent built in-process does not carry the
|
|
34
|
+
// state, and a real keyboard was not measured — so nothing here depends on
|
|
35
|
+
// it: a composed character is taken as typed, and a plain one is the
|
|
36
|
+
// accent lost rather than a letter too many.
|
|
37
|
+
// - **A key can type more than one character** — the accent and a letter it
|
|
38
|
+
// does not combine with, `´s`, where the layout composes. No code point
|
|
39
|
+
// carries two, so the whole string rides `text` and events.js commits it
|
|
40
|
+
// the way it commits an input method's text.
|
|
10
41
|
//
|
|
11
42
|
// Function and editing keys type Unicode private-use characters (U+F700…),
|
|
12
43
|
// which must never leak out as code points; the kVK table wins over the
|
|
@@ -48,30 +79,56 @@ const VK_KEYSYMS = new Map([
|
|
|
48
79
|
[111, 0xffc9], // F12
|
|
49
80
|
]);
|
|
50
81
|
|
|
51
|
-
|
|
82
|
+
// The block NSEvent's function-key constants live in (NSUpArrowFunctionKey is
|
|
83
|
+
// U+F700). Not the whole U+F700–U+F8FF range Apple reserves: U+F8FF is the
|
|
84
|
+
// Apple logo, which ⌥⇧K types and the system font draws.
|
|
85
|
+
const FUNCTION_KEY = (cp) => cp >= 0xf700 && cp <= 0xf7ff;
|
|
86
|
+
|
|
87
|
+
const printable = (cp) => cp != null && cp >= 0x20 && cp !== 0x7f;
|
|
52
88
|
|
|
53
89
|
function keysymFromChars(chars) {
|
|
54
90
|
if (!chars) return 0;
|
|
55
91
|
const cp = chars.codePointAt(0);
|
|
56
|
-
if (cp
|
|
92
|
+
if (!printable(cp) || FUNCTION_KEY(cp)) return 0;
|
|
57
93
|
return keysymOf(String.fromCodePoint(cp));
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
61
|
-
* The
|
|
62
|
-
* @windowkit/appkit payload
|
|
97
|
+
* The key facts events.js reads off a native key event, from the raw
|
|
98
|
+
* @windowkit/appkit payload: `keysym`, `baseKeysym` and `codepoint`, and
|
|
99
|
+
* `text` when the key typed more than one character.
|
|
63
100
|
*/
|
|
64
101
|
export function decodeKey(ev) {
|
|
65
102
|
const fixed = VK_KEYSYMS.get(ev.keyCode);
|
|
66
103
|
if (fixed) {
|
|
67
104
|
return { keysym: fixed, baseKeysym: fixed, codepoint: undefined };
|
|
68
105
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
const baseKeysym =
|
|
107
|
+
keysymFromChars(ev.charsBase) || keysymFromChars(ev.charsShifted);
|
|
108
|
+
// A ⌘ or ⌃ chord types nothing, and is named by its key: ⌘⌥S is a chord
|
|
109
|
+
// on S, not on ß. (`chars` for ⌃S is a control character besides.)
|
|
110
|
+
// `chars` is absent from a bridge older than the field, and from a test's
|
|
111
|
+
// event that only says which key it was.
|
|
112
|
+
const chord = ev.command || ev.control;
|
|
113
|
+
const typed = chord ? ev.charsShifted : (ev.chars ?? ev.charsShifted);
|
|
114
|
+
const points = typed ? Array.from(typed) : [];
|
|
115
|
+
if (points.length > 1) {
|
|
116
|
+
return {
|
|
117
|
+
keysym: keysymFromChars(ev.charsShifted) || baseKeysym,
|
|
118
|
+
baseKeysym,
|
|
119
|
+
codepoint: undefined,
|
|
120
|
+
text: typed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const keysym = keysymFromChars(typed);
|
|
124
|
+
const cp = typed?.codePointAt(0);
|
|
125
|
+
const codepoint = printable(cp) && !FUNCTION_KEY(cp) ? cp : undefined;
|
|
126
|
+
return {
|
|
127
|
+
// a dead key typed nothing; the key it sits on is what a chord names
|
|
128
|
+
keysym: keysym || keysymFromChars(ev.charsShifted) || baseKeysym,
|
|
129
|
+
baseKeysym,
|
|
130
|
+
codepoint,
|
|
131
|
+
};
|
|
75
132
|
}
|
|
76
133
|
|
|
77
134
|
/** AppKit modifier booleans -> the X-style state mask events carry. */
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -31,6 +31,12 @@ const ASKED_KEPT = 16;
|
|
|
31
31
|
* drag itself. */
|
|
32
32
|
const GESTURE_GAP_MS = 500;
|
|
33
33
|
|
|
34
|
+
/** How far inside the budget a frame of a pane that fell behind has to
|
|
35
|
+
* come for its frames to be anchored again (`_answered`): a pane whose
|
|
36
|
+
* frames come near the budget keeps one look rather than taking the
|
|
37
|
+
* other at every frame. */
|
|
38
|
+
const KEPT_UP = 0.75;
|
|
39
|
+
|
|
34
40
|
/** One shared surface on glass: a sublayer of a window's root layer, its
|
|
35
41
|
* contents the IOSurface the pane last presented into it. */
|
|
36
42
|
class PaneLayer {
|
|
@@ -54,6 +60,10 @@ class PaneLayer {
|
|
|
54
60
|
this._asked = [];
|
|
55
61
|
this._askedAt = null;
|
|
56
62
|
this._behind = false;
|
|
63
|
+
// when a frame last answered a size, and how many frames in a row came
|
|
64
|
+
// later than the budget (`_answered`)
|
|
65
|
+
this._answeredAt = -Infinity;
|
|
66
|
+
this._late = 0;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -73,13 +83,28 @@ class PaneLayer {
|
|
|
73
83
|
* size may take before the old one is shown in its place: 0 stretches
|
|
74
84
|
* always, as before. A bridge that predates `contentsGravity` ignores it,
|
|
75
85
|
* and stretches.
|
|
86
|
+
*
|
|
87
|
+
* Behind is a pane whose frames come later than the budget, not one with
|
|
88
|
+
* a size waiting that long. `<Frame>` sends a pane the newest size once
|
|
89
|
+
* it has painted the last, so the sizes a drag passes through while a
|
|
90
|
+
* frame is being painted are never sent, and never answered: a size
|
|
91
|
+
* waits until a frame of a later one lands, two pane frames at most. A
|
|
92
|
+
* page that painted in 27 ms answered every size it was sent in 31, and
|
|
93
|
+
* still had one 52 ms old at every step of the drag — so its layer was
|
|
94
|
+
* stretched at every step and anchored again at every frame, the page
|
|
95
|
+
* scaling and coming back thirty times a second. Here a size waiting
|
|
96
|
+
* longer than the budget counts only when no frame has answered any
|
|
97
|
+
* size in that time, which is a pane that stopped, and a frame that
|
|
98
|
+
* lands decides the rest (`_answered`).
|
|
76
99
|
*/
|
|
77
100
|
_gravityNow() {
|
|
78
101
|
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
79
102
|
if (!(budget > 0)) return 'resize';
|
|
80
|
-
// a size asked longer ago than the budget, and no frame of
|
|
103
|
+
// a size asked longer ago than the budget, and no frame of any since
|
|
81
104
|
const oldest = this._asked[0];
|
|
82
|
-
if (oldest && now() - oldest.at > budget)
|
|
105
|
+
if (oldest && now() - Math.max(oldest.at, this._answeredAt) > budget) {
|
|
106
|
+
this._behind = true;
|
|
107
|
+
}
|
|
83
108
|
return this._behind ? 'resize' : 'topLeft';
|
|
84
109
|
}
|
|
85
110
|
|
|
@@ -106,20 +131,35 @@ class PaneLayer {
|
|
|
106
131
|
return { contentsScale: scale };
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
134
|
+
/**
|
|
135
|
+
* A frame of `width` by `height` device px landed: what it answers of
|
|
136
|
+
* the sizes asked, and how long it took. Two frames in a row later than
|
|
137
|
+
* the budget put the pane behind, and one well inside it — `KEPT_UP` of
|
|
138
|
+
* it — brings it back: a frame the collector held up stretches nothing,
|
|
139
|
+
* and a pane whose frames come near the budget is not stretched and
|
|
140
|
+
* anchored by turns.
|
|
141
|
+
*/
|
|
111
142
|
_answered(width, height) {
|
|
112
143
|
const asked = this._asked;
|
|
113
144
|
// the pane rounds the logical size it was sent back to device pixels
|
|
114
145
|
const slack = Math.max(2, this.wnd.scale ?? 1);
|
|
115
|
-
|
|
146
|
+
// the last time it was asked: a drag back and forth asks a size twice
|
|
147
|
+
const at = asked.findLastIndex(
|
|
116
148
|
(a) =>
|
|
117
149
|
Math.abs(a.width - width) <= slack &&
|
|
118
150
|
Math.abs(a.height - height) <= slack,
|
|
119
151
|
);
|
|
120
152
|
if (at < 0) return;
|
|
121
|
-
this.
|
|
122
|
-
|
|
153
|
+
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
154
|
+
const took = now() - asked[at].at;
|
|
155
|
+
this._answeredAt = now();
|
|
156
|
+
if (took > budget) {
|
|
157
|
+
this._late += 1;
|
|
158
|
+
if (this._late >= 2) this._behind = true;
|
|
159
|
+
} else {
|
|
160
|
+
this._late = 0;
|
|
161
|
+
if (took <= budget * KEPT_UP) this._behind = false;
|
|
162
|
+
}
|
|
123
163
|
asked.splice(0, at + 1);
|
|
124
164
|
}
|
|
125
165
|
|
|
@@ -144,6 +184,7 @@ class PaneLayer {
|
|
|
144
184
|
if (at - (this._askedAt ?? -Infinity) >= GESTURE_GAP_MS) {
|
|
145
185
|
this._asked = [];
|
|
146
186
|
this._behind = false;
|
|
187
|
+
this._late = 0;
|
|
147
188
|
}
|
|
148
189
|
this._askedAt = at;
|
|
149
190
|
if (this._asked.length === ASKED_KEPT) this._asked.shift();
|
|
@@ -172,16 +213,20 @@ class PaneLayer {
|
|
|
172
213
|
* A pane-present landed: scan out of the named shared surface.
|
|
173
214
|
*
|
|
174
215
|
* A present names a buffer the pane may since have retired. The channel
|
|
175
|
-
* is a queue
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
216
|
+
* is a queue, so a pane-rect that changes the pane's size (or its scale —
|
|
217
|
+
* the host window moved to another display during startup, which is how
|
|
218
|
+
* this was first hit) can cross a present already in flight: the pane
|
|
219
|
+
* rebuilds its ring on that rect (`CocoaPaneWindow._ensureSurface`)
|
|
220
|
+
* before the host has looked the id up. The id is all the present
|
|
221
|
+
* carries, and the system gives a freed id to the next surface made, so
|
|
222
|
+
* the pane keeps such a buffer until the host says it has looked it up
|
|
223
|
+
* (`pane-shown`, which `CocoaPaneHost` sends once it has) — the lookup finds
|
|
224
|
+
* the frame the present named, a size behind, and the full frame on the
|
|
225
|
+
* fresh ring is queued behind it. Where the buffer is gone anyway — a
|
|
226
|
+
* pane that freed it for want of an answer, or one that died with
|
|
227
|
+
* presents in the channel — the present is dropped, and the layer keeps
|
|
228
|
+
* the frame it already holds a reference to. Nothing else here throws;
|
|
229
|
+
* any other error is the bug it says it is.
|
|
185
230
|
*/
|
|
186
231
|
present(iosurfaceId, size = null) {
|
|
187
232
|
if (this.destroyed) return false;
|
|
@@ -260,6 +305,28 @@ export class CocoaPaneHost extends PaneLayer {
|
|
|
260
305
|
this._players = new Map();
|
|
261
306
|
}
|
|
262
307
|
|
|
308
|
+
/**
|
|
309
|
+
* The pane's own present, scanned out (`PaneLayer.present`), and the pane
|
|
310
|
+
* told: whatever the buffer it named holds is the layer's now, or was
|
|
311
|
+
* dropped, so the pane may free it (`CocoaPaneWindow._releaseRing`).
|
|
312
|
+
*/
|
|
313
|
+
present(iosurfaceId, size = null) {
|
|
314
|
+
const shown = super.present(iosurfaceId, size);
|
|
315
|
+
this._acknowledge(size?.seq, null);
|
|
316
|
+
return shown;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `pane-shown`: the host has looked up the presents to `seq` of the
|
|
320
|
+
* pane's own window, or of the one `window` names. */
|
|
321
|
+
_acknowledge(seq, window) {
|
|
322
|
+
if (this.destroyed || seq == null) return;
|
|
323
|
+
this._send?.({
|
|
324
|
+
type: 'pane-shown',
|
|
325
|
+
seq,
|
|
326
|
+
...(window != null && { window }),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
263
330
|
/**
|
|
264
331
|
* A `pane-player` message: one of the pane's `<video src>`s made, steered
|
|
265
332
|
* or let go of its player. A player that cannot be made says so the way
|
|
@@ -375,9 +442,15 @@ export class CocoaPaneHost extends PaneLayer {
|
|
|
375
442
|
/** A pane-present naming one of the pane's other windows. */
|
|
376
443
|
presentWindow(msg) {
|
|
377
444
|
const entry = this._windows.get(msg.window);
|
|
378
|
-
if (!entry || this.destroyed)
|
|
445
|
+
if (!entry || this.destroyed) {
|
|
446
|
+
this._acknowledge(msg.seq, msg.window);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
379
449
|
entry.layer.setRect({ x: 0, y: 0, width: msg.width, height: msg.height });
|
|
380
|
-
|
|
450
|
+
const shown = entry.layer.present(msg.id);
|
|
451
|
+
// its ring is the pane's own ring, retired the same way
|
|
452
|
+
this._acknowledge(msg.seq, msg.window);
|
|
453
|
+
if (!shown) return;
|
|
381
454
|
entry.presented = true;
|
|
382
455
|
// a transparent window's shadow is the shape of what it shows
|
|
383
456
|
entry.wnd._shadowAfterFlip?.();
|
package/src/cocoa/paneplayer.js
CHANGED
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
// other: the host writes the buffer after the one it last named, so the one
|
|
19
19
|
// the pane is told about is not written again for two frames — some 66ms at
|
|
20
20
|
// 30fps, against a pane that copies within a display frame of hearing.
|
|
21
|
+
//
|
|
22
|
+
// A buffer is named by its IOSurface id, and the system gives a freed id to
|
|
23
|
+
// the next surface anyone makes, in either process. So the pane answers
|
|
24
|
+
// each frame once it has looked the buffer up (`seen`), the host keeps a
|
|
25
|
+
// buffer of a ring it retired until the frame naming it is answered
|
|
26
|
+
// (`HostedPlayer._freeRing`), and the pane keeps nothing a lookup found at
|
|
27
|
+
// another size than the frame says (`PanePlayer._lookUp`) — the pane's own
|
|
28
|
+
// presents, the other way round, are kept the same way
|
|
29
|
+
// (`CocoaPaneWindow._releaseRing`).
|
|
21
30
|
|
|
22
31
|
import { PlayerState } from './player.js';
|
|
23
32
|
|
|
@@ -113,27 +122,13 @@ export class PanePlayer extends PlayerState {
|
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
/** The host named a buffer with a new frame in it. */
|
|
116
|
-
_frame({ surface, width, height, time }) {
|
|
125
|
+
_frame({ surface, width, height, time, seq }) {
|
|
117
126
|
if (this.released) return;
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
) {
|
|
124
|
-
this._dropBuffers();
|
|
125
|
-
this._bufferSize = { width, height };
|
|
126
|
-
}
|
|
127
|
-
let handle = this._buffers.get(surface);
|
|
128
|
-
if (!handle) {
|
|
129
|
-
try {
|
|
130
|
-
handle = this._native.surfaceFromIOSurfaceID(surface, 1).handle;
|
|
131
|
-
} catch {
|
|
132
|
-
// freed before it got here: the next frame is in another buffer
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
this._buffers.set(surface, handle);
|
|
136
|
-
}
|
|
127
|
+
const handle = this._lookUp(surface, width, height);
|
|
128
|
+
// this frame's lookup is done, whatever it found: the host may free the
|
|
129
|
+
// buffer, which it kept for it (`HostedPlayer._freeRing`)
|
|
130
|
+
if (seq != null) this._post({ op: 'seen', seq });
|
|
131
|
+
if (!handle) return;
|
|
137
132
|
this._shown = { handle, width, height, time };
|
|
138
133
|
this._fresh = true;
|
|
139
134
|
// a playing `<video>` asks every display frame; a paused one asks when
|
|
@@ -148,6 +143,40 @@ export class PanePlayer extends PlayerState {
|
|
|
148
143
|
}
|
|
149
144
|
}
|
|
150
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The buffer the host named, looked up the first time it is named; null
|
|
148
|
+
* where the lookup found none, or found a surface of another size than
|
|
149
|
+
* the frame, which is not the frame's buffer but whatever the id has gone
|
|
150
|
+
* to since (`HostedPlayer._freeRing` says how) — never kept, and never
|
|
151
|
+
* copied out of.
|
|
152
|
+
*/
|
|
153
|
+
_lookUp(surface, width, height) {
|
|
154
|
+
// a ring of another size is a new ring: the old one's buffers are the
|
|
155
|
+
// host's to free, and these handles only keep them alive
|
|
156
|
+
if (
|
|
157
|
+
this._bufferSize?.width !== width ||
|
|
158
|
+
this._bufferSize?.height !== height
|
|
159
|
+
) {
|
|
160
|
+
this._dropBuffers();
|
|
161
|
+
this._bufferSize = { width, height };
|
|
162
|
+
}
|
|
163
|
+
const known = this._buffers.get(surface);
|
|
164
|
+
if (known) return known;
|
|
165
|
+
let found;
|
|
166
|
+
try {
|
|
167
|
+
found = this._native.surfaceFromIOSurfaceID(surface, 1);
|
|
168
|
+
} catch {
|
|
169
|
+
// freed before it got here: the next frame is in another buffer
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (found.width !== width || found.height !== height) {
|
|
173
|
+
this._native.releaseSurface?.(found.handle);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
this._buffers.set(surface, found.handle);
|
|
177
|
+
return found.handle;
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
/** As `CocoaPlayer.copyFrame`: the newest frame into `surface`, or null
|
|
152
181
|
* when the host has sent nothing since the last copy. */
|
|
153
182
|
copyFrame(surface) {
|
|
@@ -212,6 +241,12 @@ export class HostedPlayer {
|
|
|
212
241
|
this._next = 0;
|
|
213
242
|
this._want = 0;
|
|
214
243
|
this._pull = null;
|
|
244
|
+
// Each frame's number, the newest the pane has looked up (`seen`), and
|
|
245
|
+
// the buffers of retired rings a frame it has not looked up yet names:
|
|
246
|
+
// freed as it does (`_freeRing`).
|
|
247
|
+
this._seq = 0;
|
|
248
|
+
this._seenSeq = 0;
|
|
249
|
+
this._retired = [];
|
|
215
250
|
this._off = this.player._subscribe((ev) => this._event(ev));
|
|
216
251
|
}
|
|
217
252
|
|
|
@@ -221,6 +256,7 @@ export class HostedPlayer {
|
|
|
221
256
|
if (player.released) return;
|
|
222
257
|
if (msg.op === 'set') player.set(msg.settings);
|
|
223
258
|
else if (msg.op === 'seek') player.seek(msg.seconds);
|
|
259
|
+
else if (msg.op === 'seen') this._seen(msg.seq);
|
|
224
260
|
else if (msg.op === 'release') this.release();
|
|
225
261
|
}
|
|
226
262
|
|
|
@@ -274,6 +310,9 @@ export class HostedPlayer {
|
|
|
274
310
|
);
|
|
275
311
|
if (got?.written) {
|
|
276
312
|
this._next = (this._next + 1) % RING;
|
|
313
|
+
// the buffer is named in the channel until the pane says otherwise
|
|
314
|
+
// (`_freeRing`)
|
|
315
|
+
buffer.namedSeq = ++this._seq;
|
|
277
316
|
this._send({
|
|
278
317
|
type: 'pane-player-frame',
|
|
279
318
|
player: this.id,
|
|
@@ -281,6 +320,7 @@ export class HostedPlayer {
|
|
|
281
320
|
width: got.width,
|
|
282
321
|
height: got.height,
|
|
283
322
|
time: got.time,
|
|
323
|
+
seq: this._seq,
|
|
284
324
|
});
|
|
285
325
|
if (!player.playing) this._want = 0;
|
|
286
326
|
} else if (got?.width > 0 && got?.height > 0) {
|
|
@@ -316,13 +356,46 @@ export class HostedPlayer {
|
|
|
316
356
|
return this._ring[this._next];
|
|
317
357
|
}
|
|
318
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Let the ring go — all but a buffer a frame names that the pane has not
|
|
361
|
+
* looked up yet. The pane finds a buffer by its IOSurface id, and the
|
|
362
|
+
* system hands a freed id to the next surface anyone makes: the ring of
|
|
363
|
+
* the new size this same call is about to make, the pane's own ring,
|
|
364
|
+
* another process's surface. A frame still in the channel named the id,
|
|
365
|
+
* not the surface, so the pane copied out of whatever had it by the time
|
|
366
|
+
* it looked — the next ring's buffer, unwritten, shown until the next
|
|
367
|
+
* frame came, which for a paused player is the next seek. So that buffer
|
|
368
|
+
* is kept (`_retired`) until the pane says it has looked the frame up
|
|
369
|
+
* (`seen`, `_seen`). One, as a rule, since the pane answers as it reads,
|
|
370
|
+
* and never more than a ring's worth: a pane that stops answering has the
|
|
371
|
+
* oldest freed anyway, the one it is likeliest to have looked up already.
|
|
372
|
+
*/
|
|
319
373
|
_freeRing() {
|
|
374
|
+
const ring = this._ring;
|
|
375
|
+
this._ring = [];
|
|
320
376
|
const release = this._native.releaseSurface;
|
|
321
|
-
if (typeof release
|
|
322
|
-
|
|
323
|
-
|
|
377
|
+
if (typeof release !== 'function') return;
|
|
378
|
+
for (const buffer of ring) {
|
|
379
|
+
if (buffer.namedSeq > this._seenSeq) this._retired.push(buffer);
|
|
380
|
+
else release.call(this._native, buffer.handle);
|
|
324
381
|
}
|
|
325
|
-
this.
|
|
382
|
+
while (this._retired.length > RING) {
|
|
383
|
+
release.call(this._native, this._retired.shift().handle);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The pane has looked up every frame up to `seq`: the retired buffers
|
|
388
|
+
* they named go. */
|
|
389
|
+
_seen(seq) {
|
|
390
|
+
if (!(seq > this._seenSeq)) return;
|
|
391
|
+
this._seenSeq = seq;
|
|
392
|
+
if (this._retired.length === 0) return;
|
|
393
|
+
const release = this._native.releaseSurface;
|
|
394
|
+
this._retired = this._retired.filter((buffer) => {
|
|
395
|
+
if (buffer.namedSeq > seq) return true;
|
|
396
|
+
release.call(this._native, buffer.handle);
|
|
397
|
+
return false;
|
|
398
|
+
});
|
|
326
399
|
}
|
|
327
400
|
|
|
328
401
|
release() {
|
|
@@ -332,5 +405,9 @@ export class HostedPlayer {
|
|
|
332
405
|
this._want = 0;
|
|
333
406
|
this._syncPull();
|
|
334
407
|
this._freeRing();
|
|
408
|
+
// and what it kept: the pane's player is gone, or the pane is
|
|
409
|
+
for (const buffer of this._retired.splice(0)) {
|
|
410
|
+
this._native.releaseSurface(buffer.handle);
|
|
411
|
+
}
|
|
335
412
|
}
|
|
336
413
|
}
|