react-x11 2.47.2 → 2.47.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.47.2",
3
+ "version": "2.47.4",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -151,7 +151,7 @@
151
151
  "eslint-plugin-react": "^7.37.5",
152
152
  "globals": "^17.8.0",
153
153
  "hot-module-replacement": "^4.0.1",
154
- "katex": "^0.18.1",
154
+ "katex": "^0.19.0",
155
155
  "prettier": "^3.6.0",
156
156
  "react": "^19.3.0",
157
157
  "react-devtools-core": "^8.0.0",
package/src/anchor.js CHANGED
@@ -55,6 +55,26 @@ export function windowOrigin(node) {
55
55
  return win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
56
56
  }
57
57
 
58
+ /**
59
+ * The rect of the window that owns `node`, on the screen, in logical
60
+ * pixels: what a dialog is centred over. From the window rather than from
61
+ * the root's laid-out box, because a dialog that opens with its owner is
62
+ * asked for it before that box has been laid out — the window already has
63
+ * its size. Null with no window yet.
64
+ */
65
+ export function windowRect(node) {
66
+ const win = node?.root?.window;
67
+ if (!win) return null;
68
+ const origin = windowOrigin(node);
69
+ const s = node.scale ?? 1;
70
+ return {
71
+ x: origin.x / s,
72
+ y: origin.y / s,
73
+ width: win.width / s,
74
+ height: win.height / s,
75
+ };
76
+ }
77
+
58
78
  /**
59
79
  * The rect within a node that a popup is anchored to, in the node's own
60
80
  * coordinates — a caret, a table cell, a chart datapoint, the span under a
@@ -198,6 +218,11 @@ export function deviceAnchorArea(node) {
198
218
  *
199
219
  * ## Which side is which
200
220
  *
221
+ * `placement: 'center'` is no side at all: the popup goes **over** the
222
+ * anchor, the middle of one on the middle of the other, and is only clamped
223
+ * — a dialog over its owner window (`centerRect`, and `Dialog` through a
224
+ * `<popup anchor>`).
225
+ *
201
226
  * `placement: 'start'` and `'end'` are the **logical** sides, and they are
202
227
  * what a submenu wants: a menu opens away from the edge its rows begin at, so
203
228
  * it goes out to the right in an LTR menu and out to the left in an RTL one.
@@ -340,7 +365,14 @@ function placeAgainst(anchor, cross, area, s, options) {
340
365
  let x;
341
366
  let y;
342
367
 
343
- if (side === 'bottom' || side === 'top') {
368
+ if (side === 'center') {
369
+ // Over the anchor rather than beside it: the middle of the one on the
370
+ // middle of the other, on both axes. Nothing to flip to, and nothing
371
+ // for `align` or an `offset` to mean — the clamp below is the whole of
372
+ // what keeps it on the screen.
373
+ x = ax + (aw - width) / 2;
374
+ y = ay + (ah - height) / 2;
375
+ } else if (side === 'bottom' || side === 'top') {
344
376
  const below = ay + ah + offset;
345
377
  const above = ay - height - offset;
346
378
  if (
@@ -399,19 +431,15 @@ function placeAgainst(anchor, cross, area, s, options) {
399
431
  /**
400
432
  * Where to put a `<popup>` of this size **centred over the owner window**,
401
433
  * in screen coordinates, clamped into the screen. A dialog is anchored to
402
- * the window rather than to a widget, which is the one placement
403
- * `anchorRect` cannot express.
434
+ * the window rather than to a widget — `placement: 'center'` against the
435
+ * window's rect, which is what this is.
404
436
  */
405
- export function centerRect(node, { width: logicalW, height: logicalH }) {
437
+ export function centerRect(node, { width, height }) {
406
438
  if (!node) return null;
407
439
  // Logical in, logical out, device in between — anchorRect's contract,
408
440
  // for the same caller: the result becomes a popup's `x`/`y` props.
409
441
  const s = node.scale ?? 1;
410
- const width = logicalW * s;
411
- const height = logicalH * s;
412
442
  const win = node.root?.window;
413
- const ww = win?.width ?? width;
414
- const wh = win?.height ?? height;
415
443
  // `_screenOrigin` for the same reason `anchorRect` uses it: `x`/`y` come
416
444
  // from ConfigureNotify, and under a reparenting window manager those can be
417
445
  // relative to the *frame* rather than the root — which would centre the
@@ -420,19 +448,17 @@ export function centerRect(node, { width: logicalW, height: logicalH }) {
420
448
  // coordinates, so the two agree there; that is not something to rely on.)
421
449
  // The raw coordinates stay as the fallback, since the headless mock has no
422
450
  // server to ask.
423
- const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
424
- let x = origin.x + (ww - width) / 2;
425
- let y = origin.y + (wh - height) / 2;
426
-
427
- const area = deviceAnchorArea(node);
428
- if (area) {
429
- x = Math.max(area.x, Math.min(x, area.x + area.width - width));
430
- y = Math.max(area.y, Math.min(y, area.y + area.height - height));
431
- }
432
- return {
433
- x: Math.round(x) / s,
434
- y: Math.round(y) / s,
435
- width: logicalW,
436
- height: logicalH,
451
+ const origin = windowOrigin(node);
452
+ const over = {
453
+ x: origin.x,
454
+ y: origin.y,
455
+ width: win?.width ?? width * s,
456
+ height: win?.height ?? height * s,
437
457
  };
458
+ const at = placeAgainst(over, over, deviceAnchorArea(node), s, {
459
+ placement: 'center',
460
+ width,
461
+ height,
462
+ });
463
+ return { x: at.x, y: at.y, width: at.width, height: at.height };
438
464
  }
package/src/cocoa/app.js CHANGED
@@ -31,6 +31,7 @@ import { CocoaDockMenu } from './dock.js';
31
31
  import { CocoaGlobalMenuExport } from './globalmenu.js';
32
32
  import { CocoaStatusItem } from './statusitem.js';
33
33
  import { CocoaCalendars } from './calendar.js';
34
+ import { CocoaClipboard } from './clipboard.js';
34
35
  import { CocoaNotifications } from './notifications.js';
35
36
  import { CocoaPaneHost } from './panehost.js';
36
37
  import { CocoaPermissions } from './permissions.js';
@@ -38,13 +39,13 @@ import { CocoaPaneSubwindow, CocoaPaneWindow } from './panewindow.js';
38
39
  import { CocoaColorSampler } from './screencolor.js';
39
40
  import { CocoaFilePanels } from './filepanels.js';
40
41
  import { CocoaFontManager } from './fonts.js';
42
+ import { lastInputReader } from './idle.js';
41
43
  import { releaseImageUpload } from '../backend/context2d.js';
42
44
  import { PanePlayer, canShowPanePlayers } from './paneplayer.js';
43
45
  import { CocoaPlayer, canPlay } from './player.js';
44
46
  import { CocoaSurface } from './surface.js';
45
47
  import { CocoaSymbols } from './symbols.js';
46
48
  import { CocoaWindow, FRAME_SLACK_MS } from './window.js';
47
- import { utiFromMime, wire } from './dnd.js';
48
49
  import { decodeKey, modifierMask } from './keymap.js';
49
50
  import { loadNative } from './native.js';
50
51
  import { requestAppKit, threadedChannel } from './threaded.js';
@@ -345,6 +346,12 @@ export class CocoaApp {
345
346
  ? new CocoaColorSampler(native)
346
347
  : null;
347
348
 
349
+ // How long since anybody touched the Mac, which is what `useIdle()`
350
+ // asks of an app with no X server behind it (src/idle.js): the bridge's
351
+ // `lastInputMs` where it has one, `ioreg` where it does not
352
+ // (src/cocoa/idle.js, #938).
353
+ this.lastInputMs = lastInputReader(native);
354
+
348
355
  // The GL policy, glbackend.js's shape. No GLX exists here, so the
349
356
  // default is 'auto' (the direct backend where the runtime loads);
350
357
  // useSupports('shaders') stays false until the first <glarea> resolves
@@ -391,110 +398,10 @@ export class CocoaApp {
391
398
  };
392
399
  this._atoms = new Map();
393
400
 
394
- // ntk's clipboard shape over `NSPasteboard.general` — which is the one
395
- // pasteboard, and it is CLIPBOARD's. X's other everyday selection,
396
- // PRIMARY, is taken by *selecting text at all* and pasted with a middle
397
- // click, and core takes it on every selection gesture (textselection.js
398
- // `own()`, the text controls' `_ownSelection`). Sent to the pasteboard,
399
- // each of those gestures was a Copy: a drag across a paragraph replaced
400
- // the link the user had just copied in another app.
401
- //
402
- // So every other name — PRIMARY, SECONDARY, one of the app's own — is a
403
- // selection nobody on this desktop can paste from: a write resolves and
404
- // changes nothing, a read finds no owner, `targets()` is empty, `clear()`
405
- // leaves the pasteboard alone. Deliberately not a PRIMARY kept in this
406
- // process: its point is the paste in *another* application, and inside
407
- // one all it would keep is a middle click that types into a text field,
408
- // which no Mac text field does. An app that wants select-to-copy anyway
409
- // — a terminal — writes CLIPBOARD from `onSelectionChange`
410
- // (docs/clipboard.md, "Two clipboards").
411
- const isClipboard = (selection = 'CLIPBOARD') => selection === 'CLIPBOARD';
412
- this.clipboard = {
413
- // A write is one pasteboard item with every flavour under its UTI —
414
- // dnd.js's table, then the system's — the way a drag offers its
415
- // payload. Text alone keeps pasteboardWriteText, the verb every
416
- // bridge has; anything else needs pasteboardWrite (windowkit/appkit
417
- // #119), and a bridge without it keeps the text and says, once, what
418
- // it dropped — an image that silently never reaches the pasteboard
419
- // looks like a Copy that did nothing.
420
- write: (data, { selection } = {}) => {
421
- if (!isClipboard(selection)) return Promise.resolve();
422
- if (typeof data === 'string') {
423
- native.pasteboardWriteText(data);
424
- return Promise.resolve();
425
- }
426
- const item = {};
427
- for (const [type, value] of Object.entries(data ?? {})) {
428
- const uti = utiFromMime(type, native);
429
- if (uti && value != null && !(uti in item)) item[uti] = wire(value);
430
- }
431
- const TEXT = 'public.utf8-plain-text';
432
- const raw = item[TEXT];
433
- const text =
434
- raw == null
435
- ? ''
436
- : typeof raw === 'string'
437
- ? raw
438
- : Buffer.from(raw).toString('utf8');
439
- const others = Object.keys(item).filter((uti) => uti !== TEXT);
440
- if (others.length === 0) {
441
- native.pasteboardWriteText(text);
442
- } else if (typeof native.pasteboardWrite === 'function') {
443
- native.pasteboardWrite(item);
444
- } else {
445
- if (
446
- !this._warnedPasteboardFlavours &&
447
- process.env.NODE_ENV !== 'production'
448
- ) {
449
- this._warnedPasteboardFlavours = true;
450
- console.warn(
451
- `react-x11: clipboard.write() kept only the text — this ` +
452
- `@windowkit/appkit has no pasteboardWrite, so ` +
453
- `${others.join(', ')} did not reach the pasteboard. ` +
454
- 'Update @windowkit/appkit to a release with pasteboardWrite ' +
455
- '(windowkit/appkit#119).',
456
- );
457
- }
458
- native.pasteboardWriteText(text);
459
- }
460
- return Promise.resolve();
461
- },
462
- clear: (selection) => {
463
- if (isClipboard(selection)) native.pasteboardClear();
464
- return Promise.resolve();
465
- },
466
- targets: ({ selection } = {}) => {
467
- if (!isClipboard(selection)) return Promise.resolve([]);
468
- return this._ask('pasteboardReadText').then((text) =>
469
- text == null ? [] : ['UTF8_STRING', 'STRING'],
470
- );
471
- },
472
- read: ({ selection, target } = {}) => {
473
- if (!isClipboard(selection)) {
474
- return Promise.reject(
475
- new Error(
476
- `clipboard: nothing to paste — ${selection} is an X11 ` +
477
- 'selection, and on macOS only CLIPBOARD is the pasteboard',
478
- ),
479
- );
480
- }
481
- return this._ask('pasteboardReadText').then((text) => {
482
- if (text == null) {
483
- throw new Error(
484
- 'clipboard: nothing to paste — the pasteboard is empty',
485
- );
486
- }
487
- if (target === undefined) return text;
488
- if (target === 'UTF8_STRING' || target === 'STRING') {
489
- return Buffer.from(text, 'utf8');
490
- }
491
- throw new Error(
492
- `clipboard: cannot convert the pasteboard to ${target}`,
493
- );
494
- });
495
- },
496
- watch: () => Promise.resolve(() => {}),
497
- };
401
+ // ntk's clipboard shape over `NSPasteboard.general`, the one pasteboard
402
+ // and CLIPBOARD's — every other selection is one nobody here can paste
403
+ // from (src/cocoa/clipboard.js says why).
404
+ this.clipboard = new CocoaClipboard(this, native);
498
405
  }
499
406
 
500
407
  _parseColor(value) {
@@ -1912,6 +1819,7 @@ export class CocoaApp {
1912
1819
  if (this._frameTimer) clearTimeout(this._frameTimer);
1913
1820
  this._frameTimer = null;
1914
1821
  this._pollScreens(false);
1822
+ this.clipboard.close();
1915
1823
  this._cocoaGL?.destroy();
1916
1824
  this._cocoaGL = null;
1917
1825
  this._unsubscribe?.();