react-x11 2.2.0 → 2.3.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/README.md +5 -3
- package/package.json +12 -5
- package/src/Reconciler.js +96 -13
- package/src/anchor.js +15 -5
- package/src/appcontext.js +27 -7
- package/src/cocoa/app.js +557 -0
- package/src/cocoa/bezels.js +161 -0
- package/src/cocoa/context2d.js +679 -0
- package/src/cocoa/fonts.js +541 -0
- package/src/cocoa/glarea.js +321 -0
- package/src/cocoa/globalmenu.js +149 -0
- package/src/cocoa/keymap.js +86 -0
- package/src/cocoa/native.js +51 -0
- package/src/cocoa/panehost.js +50 -0
- package/src/cocoa/panewindow.js +236 -0
- package/src/cocoa/presenter.js +837 -0
- package/src/cocoa/window.js +383 -0
- package/src/components/Button.js +68 -0
- package/src/components/Checkbox.js +43 -0
- package/src/components/Radio.js +37 -1
- package/src/components/Select.js +73 -28
- package/src/components/Slider.js +59 -0
- package/src/components/Switch.js +52 -0
- package/src/components/native.js +142 -0
- package/src/decorations.js +21 -24
- package/src/editmenu.js +23 -14
- package/src/errors.js +42 -0
- package/src/events.js +11 -2
- package/src/frame/childmain.js +9 -0
- package/src/frame/index.js +162 -2
- package/src/glnodes.js +43 -0
- package/src/globalmenu.js +11 -3
- package/src/index.d.ts +10 -0
- package/src/nodes.js +165 -37
- package/src/paintcache.js +15 -4
- package/src/palette.js +14 -0
- package/src/style.d.ts +4 -1
- package/src/styles.js +48 -19
- package/src/types/elements.d.ts +10 -0
package/src/events.js
CHANGED
|
@@ -733,7 +733,16 @@ export class EventManager {
|
|
|
733
733
|
* way somewhere rather than one, and React must be free to interrupt the
|
|
734
734
|
* render it started for the notch before.
|
|
735
735
|
*/
|
|
736
|
-
|
|
736
|
+
/**
|
|
737
|
+
* @param {object} native ntk's wheel event, in this window's coordinates
|
|
738
|
+
* @param {object} [over] the node the wheel happened over, when the caller
|
|
739
|
+
* already knows. A `<glarea>` owns its own X window, so the server
|
|
740
|
+
* delivers the event *there* and the surface hands it back translated
|
|
741
|
+
* (src/glnodes.js) — and a hit test would answer with the box behind it,
|
|
742
|
+
* because a window-owning child is not in its parent's paint order.
|
|
743
|
+
* Knowing beats guessing; everything after this line is the same.
|
|
744
|
+
*/
|
|
745
|
+
_onWheel(native, over = null) {
|
|
737
746
|
// The first wheel is what says this window wants smooth scrolling. It was
|
|
738
747
|
// created on core events — an XI2 selection costs four times as many
|
|
739
748
|
// bytes per *pointer move*, which a window that is never scrolled would
|
|
@@ -747,7 +756,7 @@ export class EventManager {
|
|
|
747
756
|
// outside gets, and for the same reason (`_pressOutside`).
|
|
748
757
|
if (this._dismissOutside(native)) return;
|
|
749
758
|
runWithPriority(ContinuousEventPriority, () => {
|
|
750
|
-
const target = this._hit(native);
|
|
759
|
+
const target = over ?? this._hit(native);
|
|
751
760
|
// Shift turns a vertical wheel sideways — the convention for the mouse
|
|
752
761
|
// and the touchpad that have no horizontal axis. Read off the delta
|
|
753
762
|
// rather than off the source: a plain wheel mouse on an XI2 connection
|
package/src/frame/childmain.js
CHANGED
|
@@ -233,6 +233,15 @@ export async function runFrameChild(transport, options = {}) {
|
|
|
233
233
|
return fatal('connect', err);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// A backend that presents panes over shared memory instead of a server
|
|
237
|
+
// (the Cocoa one) takes the channel itself: geometry and input in,
|
|
238
|
+
// presents out. Feature-detected — the X11 pane path has a real window
|
|
239
|
+
// and a real server and needs none of this.
|
|
240
|
+
root.app.attachPaneChannel?.({
|
|
241
|
+
send: (msg) => transport.send(msg),
|
|
242
|
+
onMessage: (cb) => transport.onMessage(cb),
|
|
243
|
+
});
|
|
244
|
+
|
|
236
245
|
let closing = false;
|
|
237
246
|
const close = async () => {
|
|
238
247
|
if (closing) return;
|
package/src/frame/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import React, {
|
|
|
43
43
|
useState,
|
|
44
44
|
} from 'react';
|
|
45
45
|
|
|
46
|
+
import { useAppOrNull } from '../appcontext.js';
|
|
46
47
|
import { FrameEnv } from './env.js';
|
|
47
48
|
import { CallbackTable, PROTOCOL } from './protocol.js';
|
|
48
49
|
|
|
@@ -220,6 +221,11 @@ export function Frame({
|
|
|
220
221
|
ref,
|
|
221
222
|
}) {
|
|
222
223
|
const env = useContext(FrameEnv);
|
|
224
|
+
// A backend that composites panes from shared memory (Cocoa) declares
|
|
225
|
+
// itself with createPaneHost; everything else embeds the pane's real
|
|
226
|
+
// window through <foreign>, exactly as before.
|
|
227
|
+
const appOrNull = useAppOrNull();
|
|
228
|
+
const paneApp = appOrNull?.createPaneHost ? appOrNull : null;
|
|
223
229
|
const [state, setState] = useState({
|
|
224
230
|
phase: 'starting',
|
|
225
231
|
windowId: null,
|
|
@@ -365,9 +371,10 @@ export function Frame({
|
|
|
365
371
|
// rather than one frame of default before an update lands.
|
|
366
372
|
const { props: p, env: e, bridge: b } = current.current;
|
|
367
373
|
const node = containerRef.current;
|
|
374
|
+
const sc = node?.scale ?? 1;
|
|
368
375
|
const rect = {
|
|
369
|
-
width: Math.
|
|
370
|
-
height: Math.
|
|
376
|
+
width: Math.round((node?.abs?.width || 0) / sc) || 400,
|
|
377
|
+
height: Math.round((node?.abs?.height || 0) / sc) || 300,
|
|
371
378
|
};
|
|
372
379
|
s.sent = { props: p, env: e, bridge: b };
|
|
373
380
|
trySend({
|
|
@@ -413,6 +420,23 @@ export function Frame({
|
|
|
413
420
|
}, [props, env, bridge, generation]);
|
|
414
421
|
|
|
415
422
|
if (state.phase === 'running') {
|
|
423
|
+
if (paneApp) {
|
|
424
|
+
return h(PaneHostView, {
|
|
425
|
+
containerRef,
|
|
426
|
+
session,
|
|
427
|
+
app: paneApp,
|
|
428
|
+
style,
|
|
429
|
+
focusable,
|
|
430
|
+
onEmbedError: (err) => {
|
|
431
|
+
session.current?.shutdown?.();
|
|
432
|
+
setState({
|
|
433
|
+
phase: 'failed',
|
|
434
|
+
windowId: null,
|
|
435
|
+
error: Object.assign(err, { phase: 'embed' }),
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
416
440
|
return h('foreign', {
|
|
417
441
|
ref: containerRef,
|
|
418
442
|
windowId: state.windowId,
|
|
@@ -442,3 +466,139 @@ export function Frame({
|
|
|
442
466
|
: null,
|
|
443
467
|
);
|
|
444
468
|
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The Cocoa pane region: a box for layout, focus and input — hit-tested
|
|
472
|
+
* here, in the host, and forwarded over the channel — with a backend pane
|
|
473
|
+
* host object carrying the composited layer. The pane process presents by
|
|
474
|
+
* message; this view points the layer at each presented buffer.
|
|
475
|
+
*/
|
|
476
|
+
function PaneHostView({
|
|
477
|
+
containerRef,
|
|
478
|
+
session,
|
|
479
|
+
app,
|
|
480
|
+
style,
|
|
481
|
+
focusable,
|
|
482
|
+
onEmbedError,
|
|
483
|
+
}) {
|
|
484
|
+
// `onEmbedError` is a fresh closure every host render (it captures
|
|
485
|
+
// setState), and the host ticks its own state many times a second — so it
|
|
486
|
+
// must not be an effect dependency, or the effect below tears the pane
|
|
487
|
+
// layer down and rebuilds it on every host render, and the gap between
|
|
488
|
+
// `host.destroy()` and the next present is a visible full-pane flash. A
|
|
489
|
+
// ref carries the latest callback into a once-per-session effect.
|
|
490
|
+
const embedErrorRef = useRef(onEmbedError);
|
|
491
|
+
embedErrorRef.current = onEmbedError;
|
|
492
|
+
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
const s = session.current;
|
|
495
|
+
const node = containerRef.current;
|
|
496
|
+
const wnd = node?.root?.window;
|
|
497
|
+
if (!s || !node || !wnd) {
|
|
498
|
+
embedErrorRef.current?.(
|
|
499
|
+
new Error('the pane region has no window to sit in'),
|
|
500
|
+
);
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
let host;
|
|
504
|
+
try {
|
|
505
|
+
host = app.createPaneHost(wnd);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
embedErrorRef.current?.(err);
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
s.paneHost = host;
|
|
511
|
+
|
|
512
|
+
// The pane's size is this box's laid-out size, told to the pane whenever
|
|
513
|
+
// it changes — the same absolutize hook a <foreign> child window uses
|
|
514
|
+
// (src/foreignnodes.js), because onViewport fires only for scrollers.
|
|
515
|
+
let sentSize = null;
|
|
516
|
+
const syncSize = () => {
|
|
517
|
+
host.setRect(node.abs);
|
|
518
|
+
const sc = node.scale ?? 1;
|
|
519
|
+
const width = Math.max(1, Math.round(node.abs.width / sc));
|
|
520
|
+
const height = Math.max(1, Math.round(node.abs.height / sc));
|
|
521
|
+
if (sentSize && sentSize.width === width && sentSize.height === height) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
sentSize = { width, height };
|
|
525
|
+
s.trySend?.({ type: 'pane-rect', width, height, scale: sc });
|
|
526
|
+
};
|
|
527
|
+
const origAbsolutize = node.absolutize.bind(node);
|
|
528
|
+
node.absolutize = (ox, oy) => {
|
|
529
|
+
origAbsolutize(ox, oy);
|
|
530
|
+
syncSize();
|
|
531
|
+
};
|
|
532
|
+
const origShift = node._shiftAbs?.bind(node);
|
|
533
|
+
if (origShift) {
|
|
534
|
+
node._shiftAbs = (dx, dy) => {
|
|
535
|
+
origShift(dx, dy);
|
|
536
|
+
host.setRect(node.abs);
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (node.abs?.width) syncSize();
|
|
540
|
+
|
|
541
|
+
const off = s.transport.onMessage((msg) => {
|
|
542
|
+
if (msg?.type === 'pane-present') {
|
|
543
|
+
host.setRect(node.abs);
|
|
544
|
+
host.present(msg.id);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
return () => {
|
|
548
|
+
off();
|
|
549
|
+
node.absolutize = origAbsolutize;
|
|
550
|
+
if (origShift) node._shiftAbs = origShift;
|
|
551
|
+
if (s.paneHost === host) s.paneHost = null;
|
|
552
|
+
host.destroy();
|
|
553
|
+
};
|
|
554
|
+
}, [app, session, containerRef]);
|
|
555
|
+
|
|
556
|
+
const forward = (name, data) => (ev) => {
|
|
557
|
+
const s = session.current;
|
|
558
|
+
const node = containerRef.current;
|
|
559
|
+
if (!s || !node) return;
|
|
560
|
+
const sc = node.scale ?? 1;
|
|
561
|
+
const payload = {
|
|
562
|
+
x: Math.max(0, Math.round(ev.x * sc - node.abs.x)),
|
|
563
|
+
y: Math.max(0, Math.round(ev.y * sc - node.abs.y)),
|
|
564
|
+
rootx: Math.round(ev.x * sc),
|
|
565
|
+
rooty: Math.round(ev.y * sc),
|
|
566
|
+
buttons: ev.buttons ?? 0,
|
|
567
|
+
time: Date.now(),
|
|
568
|
+
...data(ev),
|
|
569
|
+
};
|
|
570
|
+
try {
|
|
571
|
+
s.trySend?.({ type: 'pane-event', name, ev: payload });
|
|
572
|
+
} catch {
|
|
573
|
+
// the exit path owns the failure story
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return h('box', {
|
|
578
|
+
ref: containerRef,
|
|
579
|
+
style,
|
|
580
|
+
focusable: focusable ?? true,
|
|
581
|
+
onMouseDown: forward('mousedown', (ev) => ({ keycode: ev.button ?? 1 })),
|
|
582
|
+
onMouseUp: forward('mouseup', (ev) => ({ keycode: ev.button ?? 1 })),
|
|
583
|
+
onMouseMove: forward('mousemove', () => ({})),
|
|
584
|
+
onWheel: forward('wheel', (ev) => ({
|
|
585
|
+
deltaX: ev.deltaX ?? 0,
|
|
586
|
+
deltaY: ev.deltaY ?? 0,
|
|
587
|
+
deltaMode: ev.deltaMode ?? 'line',
|
|
588
|
+
smooth: Boolean(ev.smooth),
|
|
589
|
+
source: 'forwarded',
|
|
590
|
+
})),
|
|
591
|
+
onKeyDown: forward('keydown', (ev) => ({
|
|
592
|
+
keysym: ev.keysym,
|
|
593
|
+
baseKeysym: ev.baseKeysym ?? ev.keysym,
|
|
594
|
+
codepoint: ev.codepoint,
|
|
595
|
+
keycode: ev.keycode ?? 0,
|
|
596
|
+
})),
|
|
597
|
+
onKeyUp: forward('keyup', (ev) => ({
|
|
598
|
+
keysym: ev.keysym,
|
|
599
|
+
baseKeysym: ev.baseKeysym ?? ev.keysym,
|
|
600
|
+
codepoint: ev.codepoint,
|
|
601
|
+
keycode: ev.keycode ?? 0,
|
|
602
|
+
})),
|
|
603
|
+
});
|
|
604
|
+
}
|
package/src/glnodes.js
CHANGED
|
@@ -194,6 +194,21 @@ export class GlAreaNode extends Node {
|
|
|
194
194
|
// GL draws into the window itself: no 2d backing pixmap, and the
|
|
195
195
|
// frame clock is ours to drive
|
|
196
196
|
backingStore: false,
|
|
197
|
+
// The wheel, and only the wheel. A GL surface owns a real X window, so
|
|
198
|
+
// the pointer events over it are delivered *there* rather than to the
|
|
199
|
+
// window the rest of the tree is hit-tested in — which is why nothing
|
|
200
|
+
// over a `<glarea>` reached an application before. Selecting it here
|
|
201
|
+
// and handing it back to the owning window's manager (`_onWheel`
|
|
202
|
+
// below) is the whole of it: from there the event is an ordinary
|
|
203
|
+
// synthetic `Wheel` at this node, so it bubbles, `preventDefault()`
|
|
204
|
+
// takes it back for a scene that zooms instead, and the default action
|
|
205
|
+
// scrolls the nearest container the way it does anywhere else.
|
|
206
|
+
//
|
|
207
|
+
// Selected unconditionally rather than when a handler is declared: the
|
|
208
|
+
// default action is what a reader expects from a wheel over a page,
|
|
209
|
+
// and an element that wants it back has `preventDefault()`. One
|
|
210
|
+
// ButtonPress per notch is not a cost worth a conditional.
|
|
211
|
+
onWheel: (ev) => this._onWheel(ev),
|
|
197
212
|
});
|
|
198
213
|
this.window = wnd;
|
|
199
214
|
this.rect = rect;
|
|
@@ -313,6 +328,34 @@ export class GlAreaNode extends Node {
|
|
|
313
328
|
if (this.props.frameLoop === 'always') this.requestFrame();
|
|
314
329
|
}
|
|
315
330
|
|
|
331
|
+
/**
|
|
332
|
+
* A wheel over the surface, handed to the window the tree lives in.
|
|
333
|
+
*
|
|
334
|
+
* ntk reports the position inside *this* window; the manager hit-tests in
|
|
335
|
+
* the owning window's space, so the node's own origin goes back on. Both
|
|
336
|
+
* are device pixels — the scale is applied at the far end, where a handler
|
|
337
|
+
* reads `ev.x` (src/events.js).
|
|
338
|
+
*
|
|
339
|
+
* Smooth deltas are not part of this yet: XI2 is selected on the window
|
|
340
|
+
* the manager owns, not on this child, so a touchpad's fractions arrive
|
|
341
|
+
* here as whole notches from buttons 4-7.
|
|
342
|
+
*/
|
|
343
|
+
_onWheel(native) {
|
|
344
|
+
const events = this.root?.events;
|
|
345
|
+
if (!events || this.destroyed) return;
|
|
346
|
+
events._onWheel(
|
|
347
|
+
{
|
|
348
|
+
...native,
|
|
349
|
+
x: (native.x ?? 0) + this.abs.x,
|
|
350
|
+
y: (native.y ?? 0) + this.abs.y,
|
|
351
|
+
},
|
|
352
|
+
// named rather than hit-tested: a window-owning child is not in its
|
|
353
|
+
// parent's paint order, so the hit test would answer with the box
|
|
354
|
+
// behind this surface
|
|
355
|
+
this,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
316
359
|
applyProps(newProps, oldProps) {
|
|
317
360
|
super.applyProps(newProps, oldProps);
|
|
318
361
|
// onDraw/clearColor are read at frame time, so any update is a new frame
|
package/src/globalmenu.js
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
snapshot,
|
|
68
68
|
IdAllocator,
|
|
69
69
|
} from './dbusmenu.js';
|
|
70
|
+
import { useAppOrNull } from './appcontext.js';
|
|
70
71
|
import { useTopLevelWindow, windowIdOf } from './windowid.js';
|
|
71
72
|
|
|
72
73
|
export const REGISTRAR_NAME = 'com.canonical.AppMenu.Registrar';
|
|
@@ -649,6 +650,7 @@ export function useGlobalMenu(
|
|
|
649
650
|
{ onSelect, onAboutToShow, enabled = true } = {},
|
|
650
651
|
) {
|
|
651
652
|
const target = useTopLevelWindow();
|
|
653
|
+
const app = useAppOrNull();
|
|
652
654
|
const [exported, setExported] = useState(false);
|
|
653
655
|
const exportRef = useRef(null);
|
|
654
656
|
|
|
@@ -661,7 +663,7 @@ export function useGlobalMenu(
|
|
|
661
663
|
|
|
662
664
|
useEffect(() => {
|
|
663
665
|
if (!enabled) return undefined;
|
|
664
|
-
const
|
|
666
|
+
const config = {
|
|
665
667
|
getMenus: () => live.current.menus ?? [],
|
|
666
668
|
onSelect: (item) => {
|
|
667
669
|
item.onSelect?.(item);
|
|
@@ -670,7 +672,13 @@ export function useGlobalMenu(
|
|
|
670
672
|
onAboutToShow: (item) => live.current.onAboutToShow?.(item),
|
|
671
673
|
target,
|
|
672
674
|
onChange: setExported,
|
|
673
|
-
}
|
|
675
|
+
};
|
|
676
|
+
// The transport is the backend's where it has one — the Cocoa backend
|
|
677
|
+
// owns the macOS menu bar and answers immediately — and D-Bus with the
|
|
678
|
+
// registrar dance everywhere else. Same owner contract either way.
|
|
679
|
+
const owner = app?.createGlobalMenuExport
|
|
680
|
+
? app.createGlobalMenuExport(config)
|
|
681
|
+
: new GlobalMenuExport(config);
|
|
674
682
|
exportRef.current = owner;
|
|
675
683
|
owner.start().catch(() => {});
|
|
676
684
|
return () => {
|
|
@@ -678,7 +686,7 @@ export function useGlobalMenu(
|
|
|
678
686
|
setExported(false);
|
|
679
687
|
owner.stop().catch(() => {});
|
|
680
688
|
};
|
|
681
|
-
}, [enabled, target]);
|
|
689
|
+
}, [enabled, target, app]);
|
|
682
690
|
|
|
683
691
|
useEffect(() => {
|
|
684
692
|
exportRef.current?.update(menus ?? []);
|
package/src/index.d.ts
CHANGED
|
@@ -170,6 +170,16 @@ export interface ErrorInfo {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
export interface RootOptions {
|
|
173
|
+
/**
|
|
174
|
+
* Which display system a root that opens its own connection talks to.
|
|
175
|
+
* `'auto'` (the default) uses the native Core Animation backend on macOS
|
|
176
|
+
* and X11 via `$DISPLAY` everywhere else; on a mac without the
|
|
177
|
+
* `@windowkit/appkit` bridge installed it falls back to X11. Naming
|
|
178
|
+
* `'cocoa'` makes the bridge required. `REACT_X11_BACKEND` overrides.
|
|
179
|
+
* Ignored when {@link RootOptions.app} is passed — a connection already
|
|
180
|
+
* is a backend.
|
|
181
|
+
*/
|
|
182
|
+
backend?: 'auto' | 'x11' | 'cocoa';
|
|
173
183
|
/** `':1'`, `'host:0.0'`, or a unix socket path. Defaults to `$DISPLAY`. */
|
|
174
184
|
display?: string;
|
|
175
185
|
/**
|