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/src/menuitem.js CHANGED
@@ -112,21 +112,62 @@ const KEY_LABELS = {
112
112
  Next: 'PgDn',
113
113
  };
114
114
 
115
+ // The Mac prints a chord as glyphs, in one fixed order whatever order the
116
+ // chord was written in — ⌃⌥⇧⌘ — and with nothing between them: `⇧⌘Z`, never
117
+ // `Cmd+Shift+Z`. `Control` and `Super` are both ⌘ there, for the reason
118
+ // `primaryModifier` gives below: a cross-platform chord is written in the
119
+ // primary modifier, and the Mac's is Command.
120
+ const MAC_MODIFIER_ORDER = ['Alt', 'Shift', 'Command'];
121
+ const MAC_MODIFIER_GLYPHS = { Alt: '⌥', Shift: '⇧', Command: '⌘' };
122
+
123
+ // The keys a Mac menu draws as a glyph rather than a word. The rest are
124
+ // `KEY_LABELS`' answer, which is already the Mac's for punctuation.
125
+ const MAC_KEY_LABELS = {
126
+ Return: '↩',
127
+ KP_Enter: '⌤',
128
+ Tab: '⇥',
129
+ BackSpace: '⌫',
130
+ Delete: '⌦',
131
+ Escape: '⎋',
132
+ Left: '←',
133
+ Right: '→',
134
+ Up: '↑',
135
+ Down: '↓',
136
+ Prior: '⇞',
137
+ Next: '⇟',
138
+ Home: '↖',
139
+ End: '↘',
140
+ };
141
+
142
+ const keyLabel = (key, labels) =>
143
+ labels[key] ??
144
+ KEY_LABELS[key] ??
145
+ (key.length === 1 ? key.toUpperCase() : key);
146
+
115
147
  /**
116
148
  * One `aas` alternative — `['Control', 'S']` — as the string a menu row
117
- * shows: `Ctrl+S`.
149
+ * shows: `Ctrl+S`, or `⌘S` where the primary modifier is ⌘.
118
150
  *
119
151
  * A single-character key is upper-cased because that is how every toolkit
120
152
  * prints one, and because dbusmenu carries the *key*, not the character it
121
153
  * types: `['Control', 's']` and `['Control', 'S']` are the same binding and
122
154
  * must not render differently.
123
155
  */
124
- function formatChord(chord) {
156
+ function formatChord(chord, primary) {
125
157
  const { modifiers, key } = splitChord(chord);
126
- const parts = [...modifiers].map((token) => MODIFIER_LABELS[token]);
127
- if (key != null) {
128
- parts.push(KEY_LABELS[key] ?? (key.length === 1 ? key.toUpperCase() : key));
158
+ if (primary === 'Super') {
159
+ const held = new Set(
160
+ [...modifiers].map((token) =>
161
+ token === 'Control' || token === 'Super' ? 'Command' : token,
162
+ ),
163
+ );
164
+ const glyphs = MAC_MODIFIER_ORDER.filter((token) => held.has(token))
165
+ .map((token) => MAC_MODIFIER_GLYPHS[token])
166
+ .join('');
167
+ return glyphs + (key == null ? '' : keyLabel(key, MAC_KEY_LABELS));
129
168
  }
169
+ const parts = [...modifiers].map((token) => MODIFIER_LABELS[token]);
170
+ if (key != null) parts.push(keyLabel(key, {}));
130
171
  return parts.join('+');
131
172
  }
132
173
 
@@ -137,11 +178,14 @@ function formatChord(chord) {
137
178
  * modifier tokens ending in the key. Only the first alternative is drawn —
138
179
  * a row has one shortcut column, and a menu that printed `Ctrl+S / F2` in it
139
180
  * would be wider than the menu for the benefit of the rarer binding.
181
+ *
182
+ * `primary` is the app's `primaryModifier` (keysyms.js): `'Super'` draws the
183
+ * chord the way a Mac menu does, `⇧⌘Z` for `['Control', 'Shift', 'Z']`.
140
184
  */
141
- export function formatShortcut(shortcut) {
185
+ export function formatShortcut(shortcut, { primary = 'Control' } = {}) {
142
186
  const first = shortcut?.[0];
143
187
  if (!Array.isArray(first) || first.length === 0) return '';
144
- return formatChord(first);
188
+ return formatChord(first, primary);
145
189
  }
146
190
 
147
191
  // What UI Events calls the modifiers, which is what `aria-keyshortcuts` is
@@ -160,19 +204,21 @@ const ARIA_MODIFIERS = {
160
204
  * dbusmenu's `aas` already has — so unlike the drawn menu, **every**
161
205
  * alternative is announced rather than only the first: a screen reader is
162
206
  * reading them out, not fitting them in a column.
207
+ *
208
+ * `primary` is `formatShortcut`'s: under `'Super'` a `Control` is ⌘, which
209
+ * UI Events calls `Meta`, so what is announced is what is drawn and bound.
163
210
  */
164
- export function ariaKeyShortcuts(shortcut) {
211
+ export function ariaKeyShortcuts(shortcut, { primary = 'Control' } = {}) {
165
212
  if (!isValidShortcut(shortcut)) return undefined;
213
+ const name = (token) => {
214
+ if (!MODIFIER_TOKENS.has(token)) return KEY_LABELS[token] ?? token;
215
+ return primary === 'Super' && token === 'Control'
216
+ ? 'Meta'
217
+ : ARIA_MODIFIERS[token];
218
+ };
219
+ // a chord naming both `Control` and `Super` is one ⌘ there, said once
166
220
  return shortcut
167
- .map((chord) =>
168
- chord
169
- .map((token) =>
170
- MODIFIER_TOKENS.has(token)
171
- ? ARIA_MODIFIERS[token]
172
- : (KEY_LABELS[token] ?? token),
173
- )
174
- .join('+'),
175
- )
221
+ .map((chord) => [...new Set(chord.map(name))].join('+'))
176
222
  .join(' ');
177
223
  }
178
224
 
package/src/node.d.ts CHANGED
@@ -73,6 +73,29 @@ export interface Context2D {
73
73
  * `'globalCompositeOperation' in ctx`.
74
74
  */
75
75
  globalCompositeOperation?: string;
76
+ /**
77
+ * Canvas's: whether an image drawn scaled or turned is smoothed, or
78
+ * drawn as its nearest pixels. Native contexts only (macOS, Windows), and
79
+ * only where the bridge can set it: assign and read back to know.
80
+ */
81
+ imageSmoothingEnabled?: boolean;
82
+ /**
83
+ * Canvas's: how it is smoothed, `'low'` being bilinear. A native context
84
+ * starts at `'medium'`, which on macOS resamples the whole source for a
85
+ * draw through a matrix whatever the clip — so a surface drawn a tile at
86
+ * a time is far cheaper at `'low'`.
87
+ */
88
+ imageSmoothingQuality?: 'low' | 'medium' | 'high';
89
+ /**
90
+ * Canvas's: a CSS filter list applied to what is drawn after it. Native
91
+ * contexts only (macOS, Windows), and only the colour functions —
92
+ * `grayscale()`, `sepia()`, `saturate()`, `hue-rotate()`, `invert()`,
93
+ * `brightness()`, `contrast()` and `opacity()` — over a bridge that reads
94
+ * a surface's pixels back. Anything else does not stick: assign and read
95
+ * back to know. Absent on X11, whose server runs no colour matrix and
96
+ * answers a read a round trip later.
97
+ */
98
+ filter?: string;
76
99
  save(): void;
77
100
  restore(): void;
78
101
  translate(x: number, y: number): void;
@@ -237,6 +260,23 @@ export interface PaintCachePlan {
237
260
  */
238
261
  export type SpriteMatrix = [number, number, number, number, number, number];
239
262
 
263
+ /**
264
+ * CSS's `matrix3d()`: sixteen numbers, column by column, a point
265
+ * (x, y, z, 1) going to the first three rows over the fourth — a turn out
266
+ * of the plane and the perspective it is seen in, about the sprite's
267
+ * `origin`. Its translation (the 13th to 15th) is in device pixels, and so
268
+ * is the distance its perspective row divides by. A presenter lifts a
269
+ * sprite with one only where none of its corners is behind the viewer, at
270
+ * rest or in any keyframe, and where its bridge takes one.
271
+ */
272
+ // prettier-ignore
273
+ export type SpriteMatrix3D = [
274
+ number, number, number, number,
275
+ number, number, number, number,
276
+ number, number, number, number,
277
+ number, number, number, number,
278
+ ];
279
+
240
280
  /** A cubic-bezier's control points `[x1, y1, x2, y2]`, `x1` and `x2` in
241
281
  * 0..1, or one of Core Animation's curve names. */
242
282
  export type SpriteTiming = [number, number, number, number] | string;
@@ -252,8 +292,9 @@ export interface SpriteAnimation {
252
292
  id: string;
253
293
  property: 'opacity' | 'transform';
254
294
  /** At least two: opacities, or matrices for a transform — sampled densely
255
- * where the element interpolates in a way the render server would not. */
256
- values: number[] | SpriteMatrix[];
295
+ * where the element interpolates in a way the render server would not.
296
+ * A transform's may be `matrix3d`s, in any mix with `matrix`es. */
297
+ values: number[] | (SpriteMatrix | SpriteMatrix3D)[];
257
298
  /** One per value, 0 to 1, never decreasing; evenly spaced when left out. */
258
299
  keyTimes?: number[];
259
300
  /** One curve per segment, so one fewer than `values`; linear when left
@@ -321,7 +362,7 @@ export interface Sprite {
321
362
  /** What the layer shows when no animation runs on it; 1 when left out. */
322
363
  opacity?: number;
323
364
  /** …and the transform, about `origin`; the identity when left out. */
324
- transform?: SpriteMatrix;
365
+ transform?: SpriteMatrix | SpriteMatrix3D;
325
366
  /** Device pixels, window coordinates; `rect`'s centre when left out. */
326
367
  origin?: { x: number; y: number };
327
368
  animations?: SpriteAnimation[];
@@ -5,6 +5,7 @@
5
5
  import { deviceAnchorArea, windowOrigin } from '../anchor.js';
6
6
  import { armPasteState, canPaste } from '../pastestate.js';
7
7
  import {
8
+ primaryModifier,
8
9
  XK_RETURN,
9
10
  XK_KP_ENTER,
10
11
  XK_UP,
@@ -125,6 +126,7 @@ export function openEditMenu(node, at, actions = {}) {
125
126
  items,
126
127
  (text) => app?.fonts?.layout(text, style)?.width,
127
128
  s,
129
+ { primary: primaryModifier(app) },
128
130
  );
129
131
  const deviceAt = at && {
130
132
  ...at,
@@ -221,6 +223,17 @@ export function openEditMenu(node, at, actions = {}) {
221
223
  node._editMenu = popup;
222
224
  // read *before* the menu takes the keyboard, and handed back on close
223
225
  node._editMenuRestore = node._focusManager()?.focused ?? null;
226
+ // The window losing focus closes it — the user went to another
227
+ // application, ⌘-Tab or a click on its window. Neither reaches us as a
228
+ // press: the grab redirects presses only between windows of ours on Cocoa,
229
+ // and a popup panel never takes the keyboard there, so this is the one
230
+ // event that says so. The node keeps its focus across the blur, so nothing
231
+ // in the tree hears it either; the same answer `Menu` and `Select` give
232
+ // (`useDismissOnWindowBlur`, components/anchor.js).
233
+ node._editMenuOffBlur =
234
+ node.root?.onWindowFocusChange?.((focused) => {
235
+ if (!focused) closeEditMenu(node);
236
+ }) ?? null;
224
237
  // the menu takes the keyboard so arrows and Escape reach it rather than
225
238
  // the element behind it
226
239
  popup.events?.focus?.(canvas);
@@ -241,6 +254,8 @@ export function closeEditMenu(node) {
241
254
  const popup = node?._editMenu;
242
255
  if (!popup) return;
243
256
  node._editMenu = null;
257
+ node._editMenuOffBlur?.();
258
+ node._editMenuOffBlur = null;
244
259
  const restore = node._editMenuRestore;
245
260
  node._editMenuRestore = null;
246
261
  // the popup is a child, so a node destroyed while its menu was up took the
@@ -255,6 +270,10 @@ export function closeEditMenu(node) {
255
270
  // menu and a ring appearing on the way back would be news to nobody. A
256
271
  // surface that was not focusable in the first place, or that stopped being
257
272
  // on screen while the menu was up, gets nothing back rather than the
258
- // destroyed menu canvas keeping the keyboard.
259
- events.focus(events._canRestoreTo(restore) ? restore : null, 'pointer');
273
+ // destroyed menu canvas keeping the keyboard. And a restore, which takes
274
+ // the window no keyboard: a menu closed because the user went to another
275
+ // application must not drag them back to this one.
276
+ events.focus(events._canRestoreTo(restore) ? restore : null, 'pointer', {
277
+ restoring: true,
278
+ });
260
279
  }
@@ -25,9 +25,12 @@ export class NodeSprites {
25
25
  * - `version`: compared with `===` from frame to frame; a change paints
26
26
  * the part again. Left out, it is painted once.
27
27
  * - `opacity`, and `transform` — CSS's `matrix(a, b, c, d, e, f)` as an
28
- * array, `e` and `f` in device pixels — about `origin`, a point in
29
- * device pixels that defaults to `rect`'s centre: the values the layer
30
- * shows when no animation is running on it.
28
+ * array, `e` and `f` in device pixels, or `matrix3d()`'s sixteen
29
+ * numbers for a part turned out of the plane and seen in a perspective
30
+ * — about `origin`, a point in device pixels that defaults to `rect`'s
31
+ * centre: the values the layer shows when no animation is running on
32
+ * it. A matrix3d part is lifted only where none of its corners is behind
33
+ * the viewer, at rest or in any keyframe.
31
34
  * - `animations`: what the render server runs on it, each `{ id,
32
35
  * property: 'opacity' | 'transform', values, keyTimes, timings,
33
36
  * duration, delay, repeat, autoreverse, hold }`, with times in
@@ -10,6 +10,7 @@ import {
10
10
  XK_PAGE_UP,
11
11
  XK_PAGE_DOWN,
12
12
  XK_END,
13
+ primaryModifierHeld,
13
14
  } from '../keysyms.js';
14
15
  import {
15
16
  scrollbarGeometry,
@@ -235,7 +236,8 @@ export class TextAreaNode extends TextInputNode {
235
236
  const layout = this._valueLayout();
236
237
 
237
238
  if (k === XK_RETURN || k === XK_KP_ENTER) {
238
- if (ev.ctrlKey) {
239
+ // Ctrl+Enter, ⌘↩ on the Cocoa backend
240
+ if (primaryModifierHeld(ev, this.app)) {
239
241
  this._fireValueEvent('onSubmit', this.value, ev.nativeEvent);
240
242
  return true;
241
243
  }
@@ -14,6 +14,7 @@ import { lastInputTime } from '../inputtime.js';
14
14
  import {
15
15
  ctrlChordLetter,
16
16
  MOD,
17
+ primaryModifierHeld,
17
18
  XK_BACKSPACE,
18
19
  XK_RETURN,
19
20
  XK_KP_ENTER,
@@ -550,7 +551,9 @@ export class TextInputNode extends Node {
550
551
  this._moveCaret(this._chars().length, ev.shiftKey);
551
552
  return true;
552
553
  }
553
- if (ev.ctrlKey) {
554
+ // Ctrl, or ⌘ on the Cocoa backend (`primaryModifierHeld`): the keys the
555
+ // edit menu prints beside its rows are the keys that do what they say
556
+ if (primaryModifierHeld(ev, this.app)) {
554
557
  const letter = ctrlChordLetter(ev);
555
558
  if (letter === 0x61 /* a */) {
556
559
  this._selectAll();
@@ -562,10 +565,12 @@ export class TextInputNode extends Node {
562
565
  } else if (letter === 0x76 /* v */) {
563
566
  this._pasteFrom();
564
567
  } else if (letter === 0x7a /* z */) {
565
- // Ctrl+Shift+Z redoes, the way it does in GTK and Qt
568
+ // Ctrl+Shift+Z redoes, the way it does in GTK and Qt — and ⇧⌘Z is
569
+ // the Mac's own redo
566
570
  if (ev.shiftKey) this.redo();
567
571
  else this.undo();
568
- } else if (letter === 0x79 /* y */) {
572
+ } else if (letter === 0x79 /* y */ && !ev.metaKey) {
573
+ // Ctrl+Y is Windows' redo; ⌘Y is nobody's, so it is not taken
569
574
  this.redo();
570
575
  } else {
571
576
  // A chord this field has no answer for is not the field's: Ctrl+S
@@ -576,6 +581,11 @@ export class TextInputNode extends Node {
576
581
  }
577
582
  return true;
578
583
  }
584
+ // …and a chord on the other one is not text either. AppKit hands over
585
+ // the key's bare character with ⌘ or ⌃ held, so ⌘S in a focused field
586
+ // typed an `s` before this, and ⌃A typed an `a` once ⌘ became the
587
+ // primary — Ctrl and Super chords are commands on every backend.
588
+ if (ev.ctrlKey || ev.metaKey) return false;
579
589
  if (ev.codepoint != null && ev.codepoint >= 0x20 && ev.codepoint !== 0x7f) {
580
590
  const ch = String.fromCodePoint(ev.codepoint);
581
591
  this._insert(ch, 'type');
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { callHandler } from './errors.js';
32
32
  import { lastInputTime } from './inputtime.js';
33
- import { ctrlChordLetter } from './keysyms.js';
33
+ import { ctrlChordLetter, primaryModifierHeld } from './keysyms.js';
34
34
  import { codePoints, wordRangeAt } from './textrange.js';
35
35
  // --- who is showing a selection ------------------------------------------
36
36
  //
@@ -201,14 +201,14 @@ export class TextSelection {
201
201
  }
202
202
 
203
203
  /**
204
- * Ctrl+A and Ctrl+C, run as this element's default action — which is why a
205
- * surface is a focus target (a11y.js): the copy has to have somewhere to
206
- * arrive. Nothing else is bound. A read-only document has no caret, so
207
- * shift+arrows would be caret browsing, which is a mode rather than a
208
- * default.
204
+ * Ctrl+A and Ctrl+C — ⌘A and ⌘C on the Cocoa backend — run as this
205
+ * element's default action, which is why a surface is a focus target
206
+ * (a11y.js): the copy has to have somewhere to arrive. Nothing else is
207
+ * bound. A read-only document has no caret, so shift+arrows would be caret
208
+ * browsing, which is a mode rather than a default.
209
209
  */
210
210
  keyDown(ev) {
211
- if (!ev.ctrlKey) return;
211
+ if (!primaryModifierHeld(ev, this.node.app)) return;
212
212
  const letter = ctrlChordLetter(ev);
213
213
  if (letter === 0x61 /* a */) {
214
214
  this.selectAll();
@@ -579,6 +579,10 @@ export interface MenuItem {
579
579
  * the menu is mounted, without the menu being opened (#351). `enabled`
580
580
  * and `visible` gate the binding with the row. `accelerators={false}` on
581
581
  * the menu turns that off for an app with a dispatcher of its own.
582
+ *
583
+ * `Control` is the primary modifier: on the Cocoa backend `[['Control',
584
+ * 'S']]` is drawn `⌘S`, bound to ⌘S, and installed in the macOS menu bar
585
+ * as ⌘S — which then answers it by itself (docs/events.md).
582
586
  */
583
587
  shortcut?: MenuShortcut;
584
588
  /** With `toggleState`, draws a check mark or a radio dot in the gutter. */
@@ -654,8 +658,10 @@ export interface MenuBarProps extends WidgetProps {
654
658
  /**
655
659
  * Honour the items' own `shortcut`s while this bar is mounted — including
656
660
  * when the desktop's panel has taken the menu over, since the panel draws
657
- * the rows but the key is pressed in this window. On by default; `false`
658
- * leaves the chords to an app that dispatches its own.
661
+ * the rows but the key is pressed in this window. The macOS menu bar is the
662
+ * exception: it answers the ⌘ chords it holds as key equivalents itself,
663
+ * and the window leaves those alone. On by default; `false` leaves the
664
+ * chords to an app that dispatches its own.
659
665
  */
660
666
  accelerators?: boolean;
661
667
  fontSize?: number;
@@ -739,7 +739,7 @@ export interface TextInputProps extends DrawnProps<TextInputNode> {
739
739
  * both read naturally.
740
740
  */
741
741
  onChange?: (ev: ChangeEvent<TextInputNode>) => void;
742
- /** Enter — or Ctrl+Enter in a `<textarea>`. */
742
+ /** Enter — or Ctrl+Enter in a `<textarea>` (⌘↩ on the Cocoa backend). */
743
743
  onSubmit?: (ev: SubmitEvent<TextInputNode>) => void;
744
744
  placeholder?: string;
745
745
  placeholderColor?: Color;
@@ -7,6 +7,7 @@
7
7
  import type { RefObject } from 'react';
8
8
  import type { DrawnNode, NtkWindow, TextInputNode } from './nodes.js';
9
9
  import type { MenuShortcut } from './components.js';
10
+ import type { PrimaryModifier } from '../keysyms.js';
10
11
 
11
12
  /** The raw ntk/X11 event a synthetic one was made from. */
12
13
  export interface NativeEvent {
@@ -331,7 +332,7 @@ export interface ChangeEvent<T = TextInputNode> extends Omit<
331
332
  }
332
333
 
333
334
  /**
334
- * `<textinput onSubmit>` — Enter, or Ctrl+Enter in a `<textarea>`. Same
335
+ * `<textinput onSubmit>` — Enter, or Ctrl+Enter in a `<textarea>` (⌘↩ on Cocoa). Same
335
336
  * shape as {@link ChangeEvent}; `nativeEvent` is the X key event.
336
337
  */
337
338
  export interface SubmitEvent<T = TextInputNode> extends Omit<
@@ -556,7 +557,8 @@ export interface AcceleratorOptions {
556
557
  * Exact on Control/Alt/Shift/Super, indifferent to Caps Lock and Num Lock,
557
558
  * matched against the Latin keysym so a layout switch does not turn it off,
558
559
  * and behind whatever a focused element consumed with `preventDefault()`.
559
- * See docs/events.md.
560
+ * `Control` is the app's primary modifier: ⌘ on the Cocoa backend, the key
561
+ * its menus print. See docs/events.md.
560
562
  *
561
563
  * With no `scope` the binding belongs to the tree's top-level `<window>`, or,
562
564
  * in an app that has none, to the root-level `<popup>` holding the keyboard —
@@ -569,10 +571,25 @@ export function useAccelerator(
569
571
  options?: AcceleratorOptions,
570
572
  ): void;
571
573
 
574
+ export interface MatchesShortcutOptions {
575
+ /**
576
+ * The app's primary modifier — `primaryModifier(app)` from
577
+ * `react-x11/keysyms`. `'Super'` reads a chord's `Control` (and its
578
+ * `Super`) as ⌘ and wants ⌃ up, which is the Cocoa backend's rule;
579
+ * `'Control'`, the default, reads the chord literally.
580
+ */
581
+ primary?: PrimaryModifier;
582
+ }
583
+
572
584
  /**
573
585
  * Whether a key event presses any alternative of a `shortcut` — the matcher
574
586
  * the menus and {@link useAccelerator} both run on, for an application
575
- * dispatching chords its own way.
587
+ * dispatching chords its own way. Pass the app's primary modifier to match
588
+ * the way they do:
589
+ *
590
+ * ```ts
591
+ * matchesShortcut(ev, [['Control', 'S']], { primary: primaryModifier(app) });
592
+ * ```
576
593
  */
577
594
  export function matchesShortcut(
578
595
  ev: {
@@ -584,4 +601,5 @@ export function matchesShortcut(
584
601
  metaKey?: boolean;
585
602
  },
586
603
  shortcut: MenuShortcut | undefined,
604
+ options?: MatchesShortcutOptions,
587
605
  ): boolean;
@@ -243,5 +243,11 @@ export interface NtkApp {
243
243
  createWindow(args?: Record<string, unknown>): NtkWindow;
244
244
  rootWindow(screen?: number): NtkWindow;
245
245
  close(): Promise<void>;
246
+ /**
247
+ * The modifier shortcuts are pressed with on this backend: `'Super'` (⌘)
248
+ * on Cocoa, absent — Ctrl — everywhere else. Read it through
249
+ * `primaryModifierHeld` from `react-x11/keysyms`.
250
+ */
251
+ readonly primaryModifier?: 'Control' | 'Super';
246
252
  [key: string]: any;
247
253
  }