react-x11 2.28.0 → 2.30.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/package.json +2 -2
- package/src/cocoa/app.js +157 -9
- package/src/cocoa/panehost.js +226 -2
- package/src/cocoa/panewindow.js +204 -14
- package/src/cocoa/promotion.js +78 -13
- package/src/cocoa/sprites.js +605 -0
- package/src/cocoa/window.js +4 -0
- package/src/components/Select.js +98 -7
- package/src/frame/index.js +60 -7
- package/src/node.d.ts +96 -0
- package/src/nodes/node.js +8 -1
- package/src/nodes/sprites.js +73 -0
- package/src/nodes/window/window.js +14 -1
- package/src/types/components.d.ts +10 -0
package/src/components/Select.js
CHANGED
|
@@ -143,6 +143,33 @@ const TRIGGER_PAD_LEFT = 10;
|
|
|
143
143
|
// (`nodes/scrollbars.js`, SCROLLBAR_WIDTH), so a menu that scrolls reserves the room
|
|
144
144
|
const SCROLLBAR_WIDTH = 6;
|
|
145
145
|
|
|
146
|
+
// The family a caption's style names, for the platform's menu: the first of
|
|
147
|
+
// a CSS list, unquoted, and nothing for a generic one, which is the menu
|
|
148
|
+
// font's own to answer.
|
|
149
|
+
const GENERIC_FAMILIES = new Set([
|
|
150
|
+
'sans-serif',
|
|
151
|
+
'serif',
|
|
152
|
+
'monospace',
|
|
153
|
+
'cursive',
|
|
154
|
+
'fantasy',
|
|
155
|
+
'system-ui',
|
|
156
|
+
'ui-sans-serif',
|
|
157
|
+
'ui-serif',
|
|
158
|
+
'ui-monospace',
|
|
159
|
+
'-apple-system',
|
|
160
|
+
'blinkmacsystemfont',
|
|
161
|
+
]);
|
|
162
|
+
function menuFamily(family) {
|
|
163
|
+
if (typeof family !== 'string') return undefined;
|
|
164
|
+
const first = family
|
|
165
|
+
.split(',')[0]
|
|
166
|
+
.trim()
|
|
167
|
+
.replace(/^["']|["']$/g, '');
|
|
168
|
+
return first && !GENERIC_FAMILIES.has(first.toLowerCase())
|
|
169
|
+
? first
|
|
170
|
+
: undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
function normalizeOption(option) {
|
|
147
174
|
return typeof option === 'object' && option !== null
|
|
148
175
|
? option
|
|
@@ -291,6 +318,17 @@ function Option({
|
|
|
291
318
|
* caption's `fontSize` is read back: the chevron is as tall as the capitals
|
|
292
319
|
* beside it. Any slot also chooses the drawn trigger over the platform's
|
|
293
320
|
* popup bezel, whose pixels no style reaches.
|
|
321
|
+
*
|
|
322
|
+
* `nativeMenu` is the menu's half of `native`: where the backend drops a
|
|
323
|
+
* control's menu as the platform's own (`app.nativePopUpMenus`, macOS), the
|
|
324
|
+
* list is that menu — the chosen row over the trigger, the platform's
|
|
325
|
+
* tracking, type-select and VoiceOver — rather than a `<popup>` drawn to
|
|
326
|
+
* look like it. It follows the trigger by default, so a select under the
|
|
327
|
+
* native bezel gets the native menu and a drawn one keeps the drawn menu;
|
|
328
|
+
* `true` asks for the platform's menu under a drawn trigger too, which is
|
|
329
|
+
* what a browser does with a `<select>` a page styled, and `false` keeps the
|
|
330
|
+
* drawn one. A drawn trigger's menu is set at the caption's size, in the
|
|
331
|
+
* family its style names when the system has it.
|
|
294
332
|
*/
|
|
295
333
|
export function Select({
|
|
296
334
|
value,
|
|
@@ -299,6 +337,7 @@ export function Select({
|
|
|
299
337
|
name,
|
|
300
338
|
placeholder = 'Select…',
|
|
301
339
|
native,
|
|
340
|
+
nativeMenu,
|
|
302
341
|
style,
|
|
303
342
|
labelStyle,
|
|
304
343
|
chevronStyle,
|
|
@@ -308,11 +347,20 @@ export function Select({
|
|
|
308
347
|
const app = useAppOrNull();
|
|
309
348
|
const styled = Boolean(labelStyle || chevronStyle);
|
|
310
349
|
const nativeControls = useNativeControls(styled ? false : native);
|
|
350
|
+
// the platform's own menu, where there is one and this select wants it
|
|
351
|
+
const platformMenu =
|
|
352
|
+
(nativeMenu ?? nativeControls) && Boolean(app?.nativePopUpMenus);
|
|
311
353
|
// The size the caption is set in, where its slot names one: the chevron is
|
|
312
354
|
// measured against those capitals, and an `<Icon>`'s size does not inherit.
|
|
313
355
|
const ownSize = flattenStyle(labelStyle).fontSize;
|
|
314
356
|
const captionSize = typeof ownSize === 'number' ? ownSize : theme.fontSize;
|
|
315
357
|
const [open, setOpen] = useState(false);
|
|
358
|
+
// The platform's menu, while it is down: its cancel function, and whether
|
|
359
|
+
// it is open, for the trigger's open look and `aria-expanded`.
|
|
360
|
+
const platformRef = useRef(null);
|
|
361
|
+
const [platformOpen, setPlatformOpen] = useState(false);
|
|
362
|
+
// a menu is down, whichever one: the trigger's open look follows it
|
|
363
|
+
const expanded = open || platformOpen;
|
|
316
364
|
const [anchor, setAnchor] = useState(null);
|
|
317
365
|
const [focused, setFocused] = useState(false);
|
|
318
366
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
@@ -387,8 +435,48 @@ export function Select({
|
|
|
387
435
|
};
|
|
388
436
|
};
|
|
389
437
|
|
|
390
|
-
const close = () =>
|
|
438
|
+
const close = () => {
|
|
439
|
+
setOpen(false);
|
|
440
|
+
platformRef.current?.();
|
|
441
|
+
};
|
|
442
|
+
// The platform's menu, dropped from the trigger: the same options, the
|
|
443
|
+
// current one placed over it. Its answer is a pick like a row's.
|
|
444
|
+
const openPlatformMenu = () => {
|
|
445
|
+
const node = triggerRef.current;
|
|
446
|
+
const wnd = node?.root?.window;
|
|
447
|
+
if (!node || !wnd || platformRef.current) return;
|
|
448
|
+
const chosen = normalized.findIndex((o) => o.value === value);
|
|
449
|
+
const family = menuFamily(flattenStyle(labelStyle).fontFamily);
|
|
450
|
+
setPlatformOpen(true);
|
|
451
|
+
platformRef.current = app.popUpMenu(
|
|
452
|
+
wnd,
|
|
453
|
+
{
|
|
454
|
+
items: normalized.map((o, i) => ({
|
|
455
|
+
id: i + 1,
|
|
456
|
+
title: String(o.label),
|
|
457
|
+
})),
|
|
458
|
+
frame: [node.abs.x, node.abs.y, node.abs.width, node.abs.height],
|
|
459
|
+
...(chosen >= 0 && { selected: chosen + 1 }),
|
|
460
|
+
// under the bezel the menu is the bezel's, at the menu font's size
|
|
461
|
+
...(!nativeControls && { fontSize: captionSize }),
|
|
462
|
+
...(!nativeControls && family && { fontFamily: family }),
|
|
463
|
+
// the theme's, as the bezel's appearance is
|
|
464
|
+
appearance: theme.scheme === 'dark' ? 'dark' : 'light',
|
|
465
|
+
rtl: node.direction === 'rtl',
|
|
466
|
+
},
|
|
467
|
+
(id) => {
|
|
468
|
+
platformRef.current = null;
|
|
469
|
+
setPlatformOpen(false);
|
|
470
|
+
const option = id != null ? normalized[id - 1] : null;
|
|
471
|
+
if (option && option.value !== value) emit(option.value);
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
};
|
|
391
475
|
const openMenu = () => {
|
|
476
|
+
if (platformMenu) {
|
|
477
|
+
openPlatformMenu();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
392
480
|
const node = triggerRef.current;
|
|
393
481
|
if (!node) return;
|
|
394
482
|
const rect = measureAnchor(menuAnchorOptions());
|
|
@@ -398,7 +486,9 @@ export function Select({
|
|
|
398
486
|
setActiveIndex(selected >= 0 ? selected : 0);
|
|
399
487
|
setOpen(true);
|
|
400
488
|
};
|
|
401
|
-
const toggle = () => (
|
|
489
|
+
const toggle = () => (expanded ? close() : openMenu());
|
|
490
|
+
// a menu still down when the select goes is taken with it
|
|
491
|
+
useEffect(() => () => platformRef.current?.(), []);
|
|
402
492
|
|
|
403
493
|
// keeps the menu under the trigger for as long as it is open: a scrolled
|
|
404
494
|
// ancestor, the trigger's own layout moving it (a neighbouring field
|
|
@@ -491,7 +581,7 @@ export function Select({
|
|
|
491
581
|
{
|
|
492
582
|
theme,
|
|
493
583
|
role: 'combobox',
|
|
494
|
-
'aria-expanded':
|
|
584
|
+
'aria-expanded': expanded,
|
|
495
585
|
'aria-haspopup': 'listbox',
|
|
496
586
|
ref: triggerRef,
|
|
497
587
|
focusable: true,
|
|
@@ -546,7 +636,8 @@ export function Select({
|
|
|
546
636
|
paddingRight: 10,
|
|
547
637
|
borderWidth: theme.borderWidth,
|
|
548
638
|
borderRadius: theme.radius,
|
|
549
|
-
borderColor:
|
|
639
|
+
borderColor:
|
|
640
|
+
focused || expanded ? theme.borderFocus : theme.border,
|
|
550
641
|
backgroundColor: theme.surface,
|
|
551
642
|
},
|
|
552
643
|
// Hover and press belong to the trigger while it is *shut*: they say
|
|
@@ -560,7 +651,7 @@ export function Select({
|
|
|
560
651
|
// not overwrite. The only way for open to win is to not be competing.
|
|
561
652
|
// (In native mode the wash overlay below answers the press instead.)
|
|
562
653
|
!nativeControls &&
|
|
563
|
-
!
|
|
654
|
+
!expanded && {
|
|
564
655
|
':hover': { backgroundColor: theme.surfaceHover },
|
|
565
656
|
':active': { backgroundColor: theme.surfaceActive },
|
|
566
657
|
},
|
|
@@ -576,7 +667,7 @@ export function Select({
|
|
|
576
667
|
nativeControls &&
|
|
577
668
|
h(Bezel, {
|
|
578
669
|
kind: 'popup',
|
|
579
|
-
pressed:
|
|
670
|
+
pressed: expanded,
|
|
580
671
|
enabled: true,
|
|
581
672
|
style: ABS_FILL,
|
|
582
673
|
}),
|
|
@@ -605,7 +696,7 @@ export function Select({
|
|
|
605
696
|
style: [
|
|
606
697
|
ABS_FILL,
|
|
607
698
|
{ borderRadius: 6 },
|
|
608
|
-
!
|
|
699
|
+
!expanded && { ':active': { backgroundColor: pressWash(theme) } },
|
|
609
700
|
],
|
|
610
701
|
})
|
|
611
702
|
: h(Icon, {
|
package/src/frame/index.js
CHANGED
|
@@ -529,7 +529,9 @@ function PaneHostView({
|
|
|
529
529
|
}
|
|
530
530
|
let host;
|
|
531
531
|
try {
|
|
532
|
-
|
|
532
|
+
// `send` is for the windows the pane makes besides its own, which a
|
|
533
|
+
// backend whose pane process cannot show one shows here (Cocoa's)
|
|
534
|
+
host = app.createPaneHost(wnd, { send: (msg) => s.trySend?.(msg) });
|
|
533
535
|
} catch (err) {
|
|
534
536
|
embedErrorRef.current?.(err);
|
|
535
537
|
return undefined;
|
|
@@ -539,17 +541,44 @@ function PaneHostView({
|
|
|
539
541
|
// The pane's size is this box's laid-out size, told to the pane whenever
|
|
540
542
|
// it changes — the same absolutize hook a <foreign> child window uses
|
|
541
543
|
// (src/foreignnodes.js), because onViewport fires only for scrollers.
|
|
542
|
-
|
|
544
|
+
//
|
|
545
|
+
// And where the box is on the screen, in device pixels (`screen`): the
|
|
546
|
+
// pane has no window of its own to ask, and a popup it anchors to one of
|
|
547
|
+
// its nodes is placed against it (`windowOrigin`, src/anchor.js) — a
|
|
548
|
+
// dropdown in a pane opened from the screen's corner without it. It
|
|
549
|
+
// moves when the box does and when the window does, so it is sent on
|
|
550
|
+
// layout, on a scroll that shifts the box, and on every anchor change
|
|
551
|
+
// of the window, which a move of the window is.
|
|
552
|
+
let sent = null;
|
|
543
553
|
const syncSize = () => {
|
|
544
554
|
host.setRect(node.abs);
|
|
545
555
|
const sc = node.scale ?? 1;
|
|
546
556
|
const width = Math.max(1, Math.round(node.abs.width / sc));
|
|
547
557
|
const height = Math.max(1, Math.round(node.abs.height / sc));
|
|
548
|
-
|
|
558
|
+
const origin = node.root?.window?._screenOrigin;
|
|
559
|
+
const screen = origin
|
|
560
|
+
? {
|
|
561
|
+
x: Math.round(origin.x + node.abs.x),
|
|
562
|
+
y: Math.round(origin.y + node.abs.y),
|
|
563
|
+
}
|
|
564
|
+
: null;
|
|
565
|
+
if (
|
|
566
|
+
sent &&
|
|
567
|
+
sent.width === width &&
|
|
568
|
+
sent.height === height &&
|
|
569
|
+
sent.screen?.x === screen?.x &&
|
|
570
|
+
sent.screen?.y === screen?.y
|
|
571
|
+
) {
|
|
549
572
|
return;
|
|
550
573
|
}
|
|
551
|
-
|
|
552
|
-
s.trySend?.({
|
|
574
|
+
sent = { width, height, screen };
|
|
575
|
+
s.trySend?.({
|
|
576
|
+
type: 'pane-rect',
|
|
577
|
+
width,
|
|
578
|
+
height,
|
|
579
|
+
scale: sc,
|
|
580
|
+
...(screen && { screen }),
|
|
581
|
+
});
|
|
553
582
|
};
|
|
554
583
|
const origAbsolutize = node.absolutize.bind(node);
|
|
555
584
|
node.absolutize = (ox, oy) => {
|
|
@@ -560,13 +589,35 @@ function PaneHostView({
|
|
|
560
589
|
if (origShift) {
|
|
561
590
|
node._shiftAbs = (dx, dy) => {
|
|
562
591
|
origShift(dx, dy);
|
|
563
|
-
|
|
592
|
+
syncSize();
|
|
564
593
|
};
|
|
565
594
|
}
|
|
595
|
+
const offAnchor = node.root?.onAnchorChange?.(syncSize);
|
|
566
596
|
if (node.abs?.width) syncSize();
|
|
567
597
|
|
|
598
|
+
// The window's focus, which the pane has no window to hear: a menu it
|
|
599
|
+
// keeps open on the strength of the window having it closes when the
|
|
600
|
+
// user goes to another application (`useDismissOnWindowBlur`).
|
|
601
|
+
const offFocus = node.root?.onWindowFocusChange?.((focused) =>
|
|
602
|
+
s.trySend?.({
|
|
603
|
+
type: 'pane-event',
|
|
604
|
+
name: focused ? 'focus' : 'blur',
|
|
605
|
+
ev: { buttons: 0, time: Date.now() },
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
|
|
568
609
|
const off = s.transport.onMessage((msg) => {
|
|
569
|
-
if (msg?.type === 'pane-
|
|
610
|
+
if (msg?.type === 'pane-window') {
|
|
611
|
+
host.paneWindow?.(msg);
|
|
612
|
+
} else if (msg?.type === 'pane-menu') {
|
|
613
|
+
host.popUpMenu?.(msg);
|
|
614
|
+
} else if (msg?.type === 'pane-menu-cancel') {
|
|
615
|
+
host.cancelMenu?.(msg.menu);
|
|
616
|
+
} else if (msg?.type === 'pane-present' && msg.window != null) {
|
|
617
|
+
host.presentWindow?.(msg);
|
|
618
|
+
} else if (msg?.type === 'pane-cursor' && msg.window != null) {
|
|
619
|
+
host.windowCursor?.(msg);
|
|
620
|
+
} else if (msg?.type === 'pane-present') {
|
|
570
621
|
host.setRect(node.abs);
|
|
571
622
|
host.present(msg.id);
|
|
572
623
|
} else if (msg?.type === 'pane-cursor') {
|
|
@@ -581,6 +632,8 @@ function PaneHostView({
|
|
|
581
632
|
});
|
|
582
633
|
return () => {
|
|
583
634
|
off();
|
|
635
|
+
offAnchor?.();
|
|
636
|
+
offFocus?.();
|
|
584
637
|
node.defaultCursor = undefined;
|
|
585
638
|
node.root?.events?.refreshCursor();
|
|
586
639
|
node.absolutize = origAbsolutize;
|
package/src/node.d.ts
CHANGED
|
@@ -230,6 +230,80 @@ export interface PaintCachePlan {
|
|
|
230
230
|
tint?: string;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* CSS's `matrix(a, b, c, d, e, f)`: a point (x, y) goes to
|
|
235
|
+
* (a·x + c·y + e, b·x + d·y + f), with `e` and `f` in device pixels.
|
|
236
|
+
*/
|
|
237
|
+
export type SpriteMatrix = [number, number, number, number, number, number];
|
|
238
|
+
|
|
239
|
+
/** A cubic-bezier's control points `[x1, y1, x2, y2]`, `x1` and `x2` in
|
|
240
|
+
* 0..1, or one of Core Animation's curve names. */
|
|
241
|
+
export type SpriteTiming = [number, number, number, number] | string;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* An animation the render server runs on a sprite: keyframes for one
|
|
245
|
+
* property, played as CSS plays an animation's iterations.
|
|
246
|
+
*/
|
|
247
|
+
export interface SpriteAnimation {
|
|
248
|
+
/** The element's own name for this run, which `spriteAnimationEnded`
|
|
249
|
+
* reports. An animation is attached once per id: to change one, give it
|
|
250
|
+
* a new id. */
|
|
251
|
+
id: string;
|
|
252
|
+
property: 'opacity' | 'transform';
|
|
253
|
+
/** At least two: opacities, or matrices for a transform — sampled densely
|
|
254
|
+
* where the element interpolates in a way the render server would not. */
|
|
255
|
+
values: number[] | SpriteMatrix[];
|
|
256
|
+
/** One per value, 0 to 1, never decreasing; evenly spaced when left out. */
|
|
257
|
+
keyTimes?: number[];
|
|
258
|
+
/** One curve per segment, so one fewer than `values`; linear when left
|
|
259
|
+
* out. */
|
|
260
|
+
timings?: SpriteTiming[];
|
|
261
|
+
/** Milliseconds, one iteration. */
|
|
262
|
+
duration: number;
|
|
263
|
+
/** Milliseconds from the frame that asks: ahead, or — negative — already
|
|
264
|
+
* that far in. Read when the animation is attached and not after. */
|
|
265
|
+
delay?: number;
|
|
266
|
+
/** Iterations, `Infinity` for a loop; one when left out. */
|
|
267
|
+
repeat?: number;
|
|
268
|
+
/** Each iteration turns round at its end, as `alternate` does. */
|
|
269
|
+
autoreverse?: boolean;
|
|
270
|
+
/** The last value stays on screen after the end. */
|
|
271
|
+
hold?: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A part of an element's drawing a presenter may put on a layer of its own
|
|
276
|
+
* — what `sprites()` answers with (docs/extending.md "Parts of your drawing
|
|
277
|
+
* on layers of their own").
|
|
278
|
+
*/
|
|
279
|
+
export interface Sprite {
|
|
280
|
+
/** The same across frames for the same part. */
|
|
281
|
+
key: string;
|
|
282
|
+
/** Its box, untransformed: device pixels, window coordinates, like
|
|
283
|
+
* `abs`. */
|
|
284
|
+
rect: Rect;
|
|
285
|
+
/** What it draws, untransformed, when that is more than `rect`. */
|
|
286
|
+
reach?: Rect;
|
|
287
|
+
/** Where it shows, when the element clips it: device pixels, window
|
|
288
|
+
* coordinates. Its layer is cut to the rect, and so it is to the clip of
|
|
289
|
+
* every ancestor with square corners (a rounded one has to hold all of
|
|
290
|
+
* it). */
|
|
291
|
+
clip?: Rect;
|
|
292
|
+
/** Draws the part where it is in the window, as `paint` would, but at
|
|
293
|
+
* opacity 1 and untransformed. */
|
|
294
|
+
paint(ctx: Context2D): void;
|
|
295
|
+
/** Compared with `===` from frame to frame; a change paints the part
|
|
296
|
+
* again. Left out, it is painted once. */
|
|
297
|
+
version?: unknown;
|
|
298
|
+
/** What the layer shows when no animation runs on it; 1 when left out. */
|
|
299
|
+
opacity?: number;
|
|
300
|
+
/** …and the transform, about `origin`; the identity when left out. */
|
|
301
|
+
transform?: SpriteMatrix;
|
|
302
|
+
/** Device pixels, window coordinates; `rect`'s centre when left out. */
|
|
303
|
+
origin?: { x: number; y: number };
|
|
304
|
+
animations?: SpriteAnimation[];
|
|
305
|
+
}
|
|
306
|
+
|
|
233
307
|
/** How an axis is bounded when layout asks an element for its size.
|
|
234
308
|
* `'exactly'` — the style decided this axis; `'at-most'` — that many pixels
|
|
235
309
|
* are on offer; `'unconstrained'` — nothing bounds it. */
|
|
@@ -570,6 +644,28 @@ export declare class Node {
|
|
|
570
644
|
* read-only.
|
|
571
645
|
*/
|
|
572
646
|
paintDamage(): Rect | null;
|
|
647
|
+
/**
|
|
648
|
+
* The parts of this element's drawing a presenter may put on layers of
|
|
649
|
+
* their own, and animate there in the render server — or null, the
|
|
650
|
+
* default. Asked every frame, after layout and before the damage is
|
|
651
|
+
* taken, by the macOS surface presenter's layer promotion and by nothing
|
|
652
|
+
* else: on X11, on Wayland and wherever a frame's scene refuses a part,
|
|
653
|
+
* the element draws everything, as it would without this.
|
|
654
|
+
*/
|
|
655
|
+
sprites(): Sprite[] | null;
|
|
656
|
+
/**
|
|
657
|
+
* The keys of this element's sprites that are on layers of their own now.
|
|
658
|
+
* Called in the frame that changes the set, before it paints; `paint`
|
|
659
|
+
* leaves a lifted part out from then on, and draws again one the set no
|
|
660
|
+
* longer holds.
|
|
661
|
+
*/
|
|
662
|
+
spritesLifted(keys: ReadonlySet<string>): void;
|
|
663
|
+
/** The render server is done with animation `id` on sprite `key`: it ran
|
|
664
|
+
* out (`finished`), or it was taken off with its layer. */
|
|
665
|
+
spriteAnimationEnded(key: string, id: string, finished: boolean): void;
|
|
666
|
+
/** Ask for the frame in which `sprites()` is asked again, when the parts
|
|
667
|
+
* changed and nothing the element draws did. A no-op where nothing asks. */
|
|
668
|
+
spritesChanged(): void;
|
|
573
669
|
/**
|
|
574
670
|
* "The pixels in `rect` moved by (dx, dy); the rest of it is new" — for an
|
|
575
671
|
* element with a viewport of its own, whose pan is a scroll in every way
|
package/src/nodes/node.js
CHANGED
|
@@ -45,6 +45,7 @@ import { NodePosition } from './position.js';
|
|
|
45
45
|
import { NodeQueries } from './queries.js';
|
|
46
46
|
import { NodeScrollBlit } from './scrollblit.js';
|
|
47
47
|
import { NodeSelectable } from './selectable.js';
|
|
48
|
+
import { NodeSprites } from './sprites.js';
|
|
48
49
|
import { NodeStyling } from './styling.js';
|
|
49
50
|
import { DEV } from './util.js';
|
|
50
51
|
|
|
@@ -608,6 +609,7 @@ export class Node {
|
|
|
608
609
|
this.root?._animating.delete(this);
|
|
609
610
|
this.root?._opaqueNodes?.delete(this);
|
|
610
611
|
this.root?._viewportFixedNodes?.delete(this);
|
|
612
|
+
this.root?._spriteNodes?.delete(this);
|
|
611
613
|
// a surface that goes away takes its selection with it, and the app-wide
|
|
612
614
|
// claim on being the one showing one goes with it too
|
|
613
615
|
this._textSelection?.destroy();
|
|
@@ -627,14 +629,18 @@ export class Node {
|
|
|
627
629
|
root?._layoutHosts?.add(this);
|
|
628
630
|
}
|
|
629
631
|
// an element answering `opaqueRect()` is one the window asks per pass,
|
|
630
|
-
//
|
|
632
|
+
// one answering `viewportFixedRects()` one it asks per scroll blit, and
|
|
633
|
+
// one answering `sprites()` one its presenter asks per frame
|
|
631
634
|
const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
|
|
632
635
|
const fixed = this.viewportFixedRects !== Node.prototype.viewportFixedRects;
|
|
636
|
+
const sprites = this.sprites !== Node.prototype.sprites;
|
|
633
637
|
if (opaque) this.root?._opaqueNodes?.delete(this);
|
|
634
638
|
if (fixed) this.root?._viewportFixedNodes?.delete(this);
|
|
639
|
+
if (sprites) this.root?._spriteNodes?.delete(this);
|
|
635
640
|
this.root = root;
|
|
636
641
|
if (opaque) root?._opaqueNodes?.add(this);
|
|
637
642
|
if (fixed) root?._viewportFixedNodes?.add(this);
|
|
643
|
+
if (sprites) root?._spriteNodes?.add(this);
|
|
638
644
|
// styled before it had a window, so this is where a placed node is
|
|
639
645
|
// first registered (WindowNode._placeNodes)
|
|
640
646
|
if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
|
|
@@ -1034,4 +1040,5 @@ installMethods(
|
|
|
1034
1040
|
NodePaint,
|
|
1035
1041
|
NodeBoxPaint,
|
|
1036
1042
|
NodeSelectable,
|
|
1043
|
+
NodeSprites,
|
|
1037
1044
|
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Sprites (issue #819): the parts of what an element draws that a retained
|
|
2
|
+
// presenter may lift onto layers of their own, and animate there in the
|
|
3
|
+
// render server. The element's half of the seam is here; the presenter's is
|
|
4
|
+
// src/cocoa/sprites.js, under layer promotion.
|
|
5
|
+
//
|
|
6
|
+
// It is the shape of `opaqueRect()`: an accessor core asks every frame, a
|
|
7
|
+
// registry of the nodes that override it (`WindowNode._spriteNodes`, kept by
|
|
8
|
+
// `_setRoot`), and a default that answers nothing, so an element that never
|
|
9
|
+
// heard of sprites pays nothing and one that answers them draws exactly what
|
|
10
|
+
// it draws today wherever nothing asks — on X11, on Wayland, on the layer
|
|
11
|
+
// presenter, and in any frame whose scene refuses a lift.
|
|
12
|
+
|
|
13
|
+
/** Node's half of sprites, installed onto `Node.prototype` by node.js. */
|
|
14
|
+
export class NodeSprites {
|
|
15
|
+
/**
|
|
16
|
+
* The parts of this element's drawing a presenter may put on layers of
|
|
17
|
+
* their own, or null, the default. Each is a plain object:
|
|
18
|
+
*
|
|
19
|
+
* - `key`: a string, the same across frames for the same part.
|
|
20
|
+
* - `rect`: its box, untransformed — device pixels, window coordinates,
|
|
21
|
+
* like `abs`.
|
|
22
|
+
* - `reach`: what it draws, untransformed, when that is more than `rect`.
|
|
23
|
+
* - `paint(ctx)`: draws it where it is in the window, as `paint` would,
|
|
24
|
+
* but at opacity 1 and untransformed: the layer carries both.
|
|
25
|
+
* - `version`: compared with `===` from frame to frame; a change paints
|
|
26
|
+
* the part again. Left out, it is painted once.
|
|
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.
|
|
31
|
+
* - `animations`: what the render server runs on it, each `{ id,
|
|
32
|
+
* property: 'opacity' | 'transform', values, keyTimes, timings,
|
|
33
|
+
* duration, delay, repeat, autoreverse, hold }`, with times in
|
|
34
|
+
* milliseconds and `delay` counted from the frame that asks — negative
|
|
35
|
+
* for one that started before it. An animation is attached once per
|
|
36
|
+
* `id`: to change one, give it a new id.
|
|
37
|
+
*
|
|
38
|
+
* Asked every frame, after layout and before the damage is taken, on a
|
|
39
|
+
* window whose presenter lifts sprites; nowhere else. An element whose
|
|
40
|
+
* parts changed while nothing was going to paint asks for that frame with
|
|
41
|
+
* `spritesChanged()`.
|
|
42
|
+
*/
|
|
43
|
+
sprites() {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The keys of this element's sprites that are on layers of their own now,
|
|
49
|
+
* a set the element may keep. Called in the frame that changes it, before
|
|
50
|
+
* that frame paints. From then on `paint` must not draw a lifted part —
|
|
51
|
+
* where it is, the window's bitmap holds a hole the layer shows through —
|
|
52
|
+
* and must draw again one the set no longer holds: a presenter gives a
|
|
53
|
+
* part back whenever the scene stops allowing it, and the bitmap under it
|
|
54
|
+
* is repainted in that same frame.
|
|
55
|
+
*/
|
|
56
|
+
spritesLifted(keys) {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The render server is done with animation `id` on sprite `key`: it ran
|
|
60
|
+
* out (`finished`), or it was taken off with its layer. Called from the
|
|
61
|
+
* backend's event, between frames.
|
|
62
|
+
*/
|
|
63
|
+
spriteAnimationEnded(key, id, finished) {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask for the frame in which this element's sprites are asked for again —
|
|
67
|
+
* one whose parts changed while nothing it drew had. Does nothing on a
|
|
68
|
+
* window whose presenter lifts none.
|
|
69
|
+
*/
|
|
70
|
+
spritesChanged() {
|
|
71
|
+
this.root?.window?.spritesChanged?.(this);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -196,6 +196,9 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
196
196
|
// the nodes answering `viewportFixedRects()`, whose rects a scroll pane
|
|
197
197
|
// they are in repairs when it blits (`_viewportFixedPins`)
|
|
198
198
|
this._viewportFixedNodes = new Set();
|
|
199
|
+
// the nodes answering `sprites()`, which a presenter that lifts parts of
|
|
200
|
+
// an element onto layers asks every frame (src/cocoa/sprites.js)
|
|
201
|
+
this._spriteNodes = new Set();
|
|
199
202
|
// Nodes that want the `attention` event (ntk#37) — an
|
|
200
203
|
// `unstable_onAttention` prop,
|
|
201
204
|
// an `:attention` block, or both. Built before the EventManager so the
|
|
@@ -547,12 +550,22 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
547
550
|
* relative to the *frame*, not the root. A popup anchored with them lands
|
|
548
551
|
* near the corner of the screen instead of under its trigger. The server
|
|
549
552
|
* will translate for us, and its answer is right whatever the WM did.
|
|
553
|
+
*
|
|
554
|
+
* A backend with no server to ask keeps the origin on its window object
|
|
555
|
+
* itself, current as of the event that moved it — Cocoa's
|
|
556
|
+
* `_nativeResized`, a frame pane's `setPaneSize`. There is no question to
|
|
557
|
+
* ask there, and what is left is what the answer would have done: tell
|
|
558
|
+
* whatever is anchored to this window that it moved. Without it a popup
|
|
559
|
+
* stayed where the window had been, and a frame pane told its new place
|
|
560
|
+
* on the screen dropped its next menu from the old one.
|
|
550
561
|
*/
|
|
551
562
|
_refreshScreenOrigin() {
|
|
552
563
|
const wnd = this.window;
|
|
564
|
+
if (!wnd) return;
|
|
553
565
|
const X = this.app?.X;
|
|
554
566
|
const root = X?.display?.screen?.[0]?.root;
|
|
555
|
-
if (
|
|
567
|
+
if (root == null || typeof X.TranslateCoordinates !== 'function') {
|
|
568
|
+
this._notifyAnchorChange();
|
|
556
569
|
return;
|
|
557
570
|
}
|
|
558
571
|
X.TranslateCoordinates(wnd.id, root, 0, 0, (err, res) => {
|
|
@@ -521,6 +521,16 @@ export interface SelectProps<T = unknown>
|
|
|
521
521
|
/** The chevron's style, over `textMuted` at the caption's cap height.
|
|
522
522
|
* `display: 'none'` leaves it out. */
|
|
523
523
|
chevronStyle?: StyleProp;
|
|
524
|
+
/**
|
|
525
|
+
* The menu's half of `native`: where the backend drops a control's menu
|
|
526
|
+
* as the platform's own (macOS, with `@windowkit/appkit` 0.20 or later),
|
|
527
|
+
* the list is that menu rather than a drawn one. Follows the trigger by
|
|
528
|
+
* default — the native bezel's select gets the native menu, a drawn one
|
|
529
|
+
* the drawn menu. `true` asks for the platform's menu under a drawn
|
|
530
|
+
* trigger too, set at the caption's size; `false` keeps the drawn menu.
|
|
531
|
+
* Ignored where the backend has no such menu.
|
|
532
|
+
*/
|
|
533
|
+
nativeMenu?: boolean;
|
|
524
534
|
}
|
|
525
535
|
export function Select<T = unknown>(props: SelectProps<T>): ReactNode;
|
|
526
536
|
|