react-x11 2.39.0 → 2.41.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.
@@ -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,15 @@ 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. 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).
296
+ this.primaryModifier = 'Super';
297
+
289
298
  // AppKit-rendered control bezels. Its *presence* is the capability:
290
299
  // `useSupports('nativeControls')` and the widget set's `controls:
291
300
  // 'auto'` policy both test for this property, so a backend without it
@@ -927,8 +936,9 @@ export class CocoaApp {
927
936
 
928
937
  /**
929
938
  * The pane's end of the frame channel (childmain hands it over,
930
- * feature-detected so the X11 pane path never notices): geometry and
931
- * input come in, pane-present and pane-cursor go out. `players` is the
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
932
942
  * host's word that it plays the pane's `<video src>`s, which makes
933
943
  * `createPlayer` here — before the pane's first render, which asks.
934
944
  */
@@ -956,7 +966,12 @@ export class CocoaApp {
956
966
  }
957
967
  const pane = this._paneWindow ?? [...this._windows.values()][0];
958
968
  if (!pane) return;
959
- if (msg?.type === 'pane-rect') {
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') {
960
975
  pane.setPaneSize(msg.width, msg.height, msg.scale, msg.screen);
961
976
  this._flushPaneOutbox();
962
977
  this._afterInput();
@@ -1203,11 +1218,12 @@ export class CocoaApp {
1203
1218
  const surfaces = [];
1204
1219
  for (const entry of queue) {
1205
1220
  // A window whose last flip has not reached its layer yet (threaded
1206
- // mode's fence, `CocoaWindow.frameInFlight`) waits before
1207
- // its clock is asked, or the clock would count a frame that did not
1208
- // run. The release is an event, and the batch it arrives in ticks
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
1209
1225
  // again.
1210
- if (entry.wnd?.frameInFlight?.()) {
1226
+ if (entry.wnd?.frameHeld?.()) {
1211
1227
  this._rafQueue.push(entry);
1212
1228
  continue;
1213
1229
  }
@@ -1553,6 +1569,8 @@ export class CocoaApp {
1553
1569
  keysym: decoded.keysym,
1554
1570
  baseKeysym: decoded.baseKeysym,
1555
1571
  codepoint: decoded.codepoint,
1572
+ // more than one character, which no code point carries (keymap.js)
1573
+ text: decoded.text,
1556
1574
  buttons: modifierMask(ev),
1557
1575
  group: 0,
1558
1576
  time: ev.time,
@@ -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 { key: key.toLowerCase(), modifiers: modifiers || FLAG_COMMAND };
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
  }
@@ -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) -> baseKeysym — what chords match
9
- // charsShifted (only Shift applied) -> keysym / codepoint — what was typed
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
- const PRIVATE_USE = (cp) => cp >= 0xf700 && cp <= 0xf8ff;
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 == null || cp < 0x20 || PRIVATE_USE(cp)) return 0;
92
+ if (!printable(cp) || FUNCTION_KEY(cp)) return 0;
57
93
  return keysymOf(String.fromCodePoint(cp));
58
94
  }
59
95
 
60
96
  /**
61
- * The three key facts events.js reads off a native key event, from the raw
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 keysym = keysymFromChars(ev.charsShifted);
70
- const baseKeysym = keysymFromChars(ev.charsBase) || keysym;
71
- const cp = ev.charsShifted?.codePointAt(0);
72
- const codepoint =
73
- cp != null && cp >= 0x20 && !PRIVATE_USE(cp) ? cp : undefined;
74
- return { keysym: keysym || baseKeysym, baseKeysym, codepoint };
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. */