react-x11 2.47.3 → 2.47.5
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/anchor.js +48 -22
- package/src/cocoa/app.js +13 -105
- package/src/cocoa/clipboard.js +485 -0
- package/src/cocoa/idle.js +92 -0
- package/src/cocoa/statusitem.js +30 -10
- package/src/components/Dialog.js +69 -28
- package/src/components/Menu.js +64 -23
- package/src/events.js +22 -4
- package/src/frame/index.js +4 -1
- package/src/idle.js +50 -20
- package/src/idlehooks.js +5 -2
- package/src/index.d.ts +14 -3
- package/src/nodes/node.js +6 -0
- package/src/nodes/textarea.js +10 -1
- package/src/nodes/textinput.js +8 -0
- package/src/nodes/window/anchoring.js +87 -2
- package/src/nodes/window/floors.js +26 -1
- package/src/nodes/window/spine.js +21 -3
- package/src/nodes/window/window.js +9 -0
- package/src/types/components.d.ts +35 -8
- package/src/types/elements.d.ts +5 -1
- package/src/types/system.d.ts +3 -1
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// A window anchored to a rect in another (#255, #280): where it goes, and
|
|
2
2
|
// following the anchor as it moves.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
anchorOffscreen,
|
|
6
|
+
anchorRect,
|
|
7
|
+
anchorScreenRect,
|
|
8
|
+
deviceAnchorArea,
|
|
9
|
+
} from '../../anchor.js';
|
|
10
|
+
import { availableArea } from '../../screens.js';
|
|
5
11
|
|
|
6
12
|
/** Anchoring, installed onto `WindowNode.prototype` by window.js. */
|
|
7
13
|
export class WindowAnchoring {
|
|
@@ -109,7 +115,10 @@ export class WindowAnchoring {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
if (lost || !size) return;
|
|
112
|
-
|
|
118
|
+
let rect = this._anchorPlacement(size);
|
|
119
|
+
if (!rect) return;
|
|
120
|
+
if (rect.placement === 'center') rect = this._keepCentredTop(rect, size);
|
|
121
|
+
this._placedSize = { width: size.width, height: size.height };
|
|
113
122
|
if (!rect) return;
|
|
114
123
|
if (this._placedAt?.x === rect.x && this._placedAt?.y === rect.y) return;
|
|
115
124
|
this._placedAt = { x: rect.x, y: rect.y };
|
|
@@ -120,6 +129,82 @@ export class WindowAnchoring {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Where a **centred** window goes when it is placed again: centred
|
|
134
|
+
* afresh when what it is centred over has moved, and otherwise — its own
|
|
135
|
+
* size changed, nothing else did — grown from where it already is.
|
|
136
|
+
*
|
|
137
|
+
* A centred popup is a dialog, and a dialog that grows is one the user is
|
|
138
|
+
* reading: an error line under a field, a "details" section opening. Re-
|
|
139
|
+
* centring it moves its top edge by half the growth, so the title, the
|
|
140
|
+
* field and the button that was just pressed all jump up under the
|
|
141
|
+
* pointer. So it keeps its top edge and its horizontal centre, grows down
|
|
142
|
+
* (and sideways about the middle), and is pulled back only as far as it
|
|
143
|
+
* takes to stay on its monitor — which is where the actions row of a
|
|
144
|
+
* dialog that grew past the bottom of the screen would otherwise go.
|
|
145
|
+
*
|
|
146
|
+
* A window the window manager frames is the manager's and the user's to
|
|
147
|
+
* move once it is up — they may have dragged it — so it grows from where
|
|
148
|
+
* the server says it is, and is moved only when it would leave the
|
|
149
|
+
* monitor. Never to where it opened: that would undo the drag.
|
|
150
|
+
*
|
|
151
|
+
* Returns the rect to place at, or null for "stay put".
|
|
152
|
+
*/
|
|
153
|
+
_keepCentredTop(rect, size) {
|
|
154
|
+
const was = this._centredFor;
|
|
155
|
+
const at = this._placedAt;
|
|
156
|
+
if (was && at) {
|
|
157
|
+
const again = this._anchorPlacement(was.size);
|
|
158
|
+
if (again && again.x === was.x && again.y === was.y) {
|
|
159
|
+
return this._growInPlace(size);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this._centredFor = { size: { ...size }, x: rect.x, y: rect.y };
|
|
163
|
+
return rect;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `_keepCentredTop`'s other half: this window at `size`, its top edge and
|
|
167
|
+
* its middle where they are, clamped into the monitor. */
|
|
168
|
+
_growInPlace(size) {
|
|
169
|
+
const managed = this.attributes.overrideRedirect === false;
|
|
170
|
+
// An override-redirect window is wherever it was last put, and the
|
|
171
|
+
// record of that is ours; a managed one may have been moved since.
|
|
172
|
+
const from = managed
|
|
173
|
+
? (this.window._screenOrigin ?? this._placedAt)
|
|
174
|
+
: this._placedAt;
|
|
175
|
+
const old = this._placedSize ?? size;
|
|
176
|
+
let x = from.x + Math.round((old.width - size.width) / 2);
|
|
177
|
+
let y = from.y;
|
|
178
|
+
const area = this._anchorArea();
|
|
179
|
+
if (area) {
|
|
180
|
+
x = Math.max(area.x, Math.min(x, area.x + area.width - size.width));
|
|
181
|
+
y = Math.max(area.y, Math.min(y, area.y + area.height - size.height));
|
|
182
|
+
}
|
|
183
|
+
// Nothing to send. For a managed window that matters beyond the round
|
|
184
|
+
// trip: a position handed back to an X window manager is read as the
|
|
185
|
+
// frame's, and would push the window down by its titlebar.
|
|
186
|
+
if (managed && x === from.x && y === from.y) return null;
|
|
187
|
+
return { x, y };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The monitor area this window is kept inside, in device pixels — the
|
|
191
|
+
* one its anchor picks (`anchorRect`, `anchorScreenRect`). */
|
|
192
|
+
_anchorArea() {
|
|
193
|
+
const anchor = this.props.anchor;
|
|
194
|
+
if (anchor.rect) {
|
|
195
|
+
const s = this.scale;
|
|
196
|
+
const r = anchor.rect;
|
|
197
|
+
return availableArea(this.app, {
|
|
198
|
+
x: r.x * s,
|
|
199
|
+
y: r.y * s,
|
|
200
|
+
width: (r.width ?? 0) * s,
|
|
201
|
+
height: (r.height ?? 0) * s,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const node = this._anchorTarget(anchor.to);
|
|
205
|
+
return node ? deviceAnchorArea(node) : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
123
208
|
/**
|
|
124
209
|
* Whether there is nothing for this window to point at: the anchor's node
|
|
125
210
|
* has scrolled out of view, or is a ref that has not attached yet — which
|
|
@@ -333,7 +333,8 @@ export function dirtyOutside(node, boundaries, paths) {
|
|
|
333
333
|
* Undefined where it cannot be summed: a child with no extent on hand, a
|
|
334
334
|
* negative margin (the boxes would overlap, and the last would not have to
|
|
335
335
|
* be the lowest), or nothing in flow at all — a leaf's answer, which is
|
|
336
|
-
* `contentSpan`'s to give.
|
|
336
|
+
* `contentSpan`'s to give. All but the first are the column's own, and a
|
|
337
|
+
* spine never sums a column that has them (`columnSums`).
|
|
337
338
|
*/
|
|
338
339
|
export function columnHeightSpan(node) {
|
|
339
340
|
const yoga = node.yoga;
|
|
@@ -363,6 +364,30 @@ export function columnHeightSpan(node) {
|
|
|
363
364
|
);
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Whether `columnHeightSpan` has an answer for this column once its children
|
|
369
|
+
* carry extents: everything else it asks — something in flow, margins down
|
|
370
|
+
* that are lengths and pull no box over the one before it, a gap that is a
|
|
371
|
+
* length — asked ahead of the measurement, from the same readings.
|
|
372
|
+
*
|
|
373
|
+
* A spine asks it before it sums a column (`isSummableColumn`), because once
|
|
374
|
+
* the measurement is under way a sum with no answer has nowhere to go: it is
|
|
375
|
+
* written as the column's extent, an extent of none writes the style's own
|
|
376
|
+
* minimum, and the column — and every column summed above it — lost its
|
|
377
|
+
* floor to a label that changed beside a block with `marginTop: -4`.
|
|
378
|
+
*/
|
|
379
|
+
export function columnSums(node) {
|
|
380
|
+
if (columnGap(node) === undefined) return false;
|
|
381
|
+
let count = 0;
|
|
382
|
+
for (const child of node.children) {
|
|
383
|
+
if (!inFlow(child)) continue;
|
|
384
|
+
const { top, bottom } = marginsDown(child);
|
|
385
|
+
if (!(top >= 0 && bottom >= 0)) return false;
|
|
386
|
+
count += 1;
|
|
387
|
+
}
|
|
388
|
+
return count !== 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
366
391
|
/**
|
|
367
392
|
* `marginDown` for both edges of a box, kept on the box with the style object
|
|
368
393
|
* it was read under. A margin reaches yoga only from the style
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
// length — is not a spine, and the tree is measured whole.
|
|
37
37
|
|
|
38
38
|
import {
|
|
39
|
+
columnSums,
|
|
39
40
|
declaresOwnMinimum,
|
|
40
41
|
inFlow,
|
|
41
42
|
mainAxisOf,
|
|
@@ -74,7 +75,9 @@ const PADDINGS = [
|
|
|
74
75
|
* main axis without wrapping, and with a height that is its content's —
|
|
75
76
|
* nothing named on the height axis (`declaresOwnMinimum`), so neither a
|
|
76
77
|
* size, a floor, a ceiling nor a clip stands between its children and its
|
|
77
|
-
* extent.
|
|
78
|
+
* extent. And one the sum has an answer for (`columnSums`): a column with a
|
|
79
|
+
* block pulled up over the one before it is measured, as a root, rather
|
|
80
|
+
* than summed into no extent and no floor.
|
|
78
81
|
*/
|
|
79
82
|
export function isSummableColumn(node) {
|
|
80
83
|
const style = node.style;
|
|
@@ -86,7 +89,8 @@ export function isSummableColumn(node) {
|
|
|
86
89
|
mainAxisOf(node) === 'height' &&
|
|
87
90
|
style.flexDirection !== 'column-reverse' &&
|
|
88
91
|
(style.flexWrap == null || style.flexWrap === 'nowrap') &&
|
|
89
|
-
!declaresOwnMinimum(node, 'height')
|
|
92
|
+
!declaresOwnMinimum(node, 'height') &&
|
|
93
|
+
columnSums(node)
|
|
90
94
|
);
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -129,6 +133,17 @@ export function spineScope(root, sources, listOnly) {
|
|
|
129
133
|
const spine = new Set();
|
|
130
134
|
const stops = new Set();
|
|
131
135
|
const offered = new OfferedWidths(root);
|
|
136
|
+
// asked once a column: a dozen rows that arrive in one column of a
|
|
137
|
+
// thousand ask it of that column a dozen times (`columnSums`)
|
|
138
|
+
const summable = new Map();
|
|
139
|
+
const sums = (column) => {
|
|
140
|
+
let answer = summable.get(column);
|
|
141
|
+
if (answer === undefined) {
|
|
142
|
+
answer = isSummableColumn(column);
|
|
143
|
+
summable.set(column, answer);
|
|
144
|
+
}
|
|
145
|
+
return answer;
|
|
146
|
+
};
|
|
132
147
|
for (const source of sources) {
|
|
133
148
|
const list = listOnly(source);
|
|
134
149
|
// the way up to where the change stops mattering
|
|
@@ -148,7 +163,7 @@ export function spineScope(root, sources, listOnly) {
|
|
|
148
163
|
let top = -1;
|
|
149
164
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
150
165
|
const summed = i > 0 || list;
|
|
151
|
-
if (!summed || !
|
|
166
|
+
if (!summed || !sums(path[i])) {
|
|
152
167
|
top = i;
|
|
153
168
|
break;
|
|
154
169
|
}
|
|
@@ -379,6 +394,9 @@ function rootWidths(node, offered, stop) {
|
|
|
379
394
|
if (!stretched(node)) return null;
|
|
380
395
|
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
381
396
|
if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
|
|
397
|
+
// …nor a gap that is a share of something: laid out alone, it is a share
|
|
398
|
+
// of something else
|
|
399
|
+
if (typeof (style.rowGap ?? style.gap) === 'string') return null;
|
|
382
400
|
return offered.in(parent, stop);
|
|
383
401
|
}
|
|
384
402
|
|
|
@@ -343,6 +343,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
343
343
|
attributes.x = placed.x;
|
|
344
344
|
attributes.y = placed.y;
|
|
345
345
|
this._placedAt = { x: placed.x, y: placed.y };
|
|
346
|
+
this._placedSize = { width: natural.width, height: natural.height };
|
|
347
|
+
// what a centred window was centred at, so a later change of its own
|
|
348
|
+
// size grows it from here rather than centring it again
|
|
349
|
+
// (`_keepCentredTop`)
|
|
350
|
+
if (placed.placement === 'center') {
|
|
351
|
+
this._centredFor = { size: this._placedSize, x: placed.x, y: placed.y };
|
|
352
|
+
}
|
|
346
353
|
}
|
|
347
354
|
// A bound the content decides is a number by now, and the window manager
|
|
348
355
|
// reads `WM_NORMAL_HINTS` when it frames the window — so it goes in with
|
|
@@ -923,6 +930,8 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
923
930
|
this._followAnchor();
|
|
924
931
|
} else if (before.anchor) {
|
|
925
932
|
this._placedAt = null;
|
|
933
|
+
this._placedSize = null;
|
|
934
|
+
this._centredFor = null;
|
|
926
935
|
// A popup that gives up its anchor gives up being hidden by one: it
|
|
927
936
|
// is an ordinary `x`/`y` popup from here, and the configure above
|
|
928
937
|
// has already put it where the app asked.
|
|
@@ -495,8 +495,22 @@ export interface DialogProps extends WidgetProps {
|
|
|
495
495
|
managed?: boolean;
|
|
496
496
|
onClose?: () => void;
|
|
497
497
|
actions?: ReactNode;
|
|
498
|
-
|
|
499
|
-
|
|
498
|
+
/**
|
|
499
|
+
* The dialog's width, 360 by default — a number, because a body of prose
|
|
500
|
+
* wraps to it, where a natural width would be as wide as its longest
|
|
501
|
+
* sentence. `'auto'` sizes it to its content, capped at the monitor.
|
|
502
|
+
*/
|
|
503
|
+
width?: number | 'auto';
|
|
504
|
+
/**
|
|
505
|
+
* `'auto'` (the default) is the content's height: the dialog is measured,
|
|
506
|
+
* centred over its owner at that size and only then opened, and it keeps
|
|
507
|
+
* following its content while open, growing from its top edge rather than
|
|
508
|
+
* being centred again. Capped at the usable height of the monitor; past
|
|
509
|
+
* that the body scrolls and the `actions` row stays pinned at the bottom
|
|
510
|
+
* of the window. A number is that height exactly, with the same scrolling
|
|
511
|
+
* body when the content needs more.
|
|
512
|
+
*/
|
|
513
|
+
height?: number | 'auto';
|
|
500
514
|
style?: StyleProp;
|
|
501
515
|
}
|
|
502
516
|
export const Dialog: ComponentType<DialogProps>;
|
|
@@ -672,7 +686,14 @@ export const MenuBar: ComponentType<MenuBarProps>;
|
|
|
672
686
|
export interface TabItem {
|
|
673
687
|
id: string;
|
|
674
688
|
label: string;
|
|
675
|
-
|
|
689
|
+
/**
|
|
690
|
+
* The panel shown while this tab is selected. A function is the lazy form:
|
|
691
|
+
* it is called with no arguments, only while its tab is selected, and again
|
|
692
|
+
* on each render of `Tabs` while it stays selected — nothing is memoised, so
|
|
693
|
+
* a hidden tab's panel is never built. Leave it out to use `Tabs` as a pure
|
|
694
|
+
* navigator whose panel lives elsewhere.
|
|
695
|
+
*/
|
|
696
|
+
content?: ReactNode | (() => ReactNode);
|
|
676
697
|
disabled?: boolean;
|
|
677
698
|
}
|
|
678
699
|
|
|
@@ -748,12 +769,17 @@ export const SplitPane: ComponentType<SplitPaneProps>;
|
|
|
748
769
|
/**
|
|
749
770
|
* The side a popup is asked for. `'start'`/`'end'` are the **logical** ones
|
|
750
771
|
* and mirror with the anchor's direction, which is what a submenu wants;
|
|
751
|
-
* `'left'`/`'right'` stay physical.
|
|
772
|
+
* `'left'`/`'right'` stay physical. `'center'` is no side: the popup goes
|
|
773
|
+
* over the anchor, middle on middle, and is only clamped into the monitor —
|
|
774
|
+
* what `Dialog` uses over its owner window. On a `<popup anchor>` a centred
|
|
775
|
+
* popup is centred when it opens and grows from its top edge when its own
|
|
776
|
+
* size changes afterwards.
|
|
752
777
|
*/
|
|
753
|
-
export type AnchorPlacement = Placement | 'start' | 'end';
|
|
778
|
+
export type AnchorPlacement = Placement | 'start' | 'end' | 'center';
|
|
754
779
|
|
|
755
780
|
export interface AnchorOptions {
|
|
756
|
-
/** Preferred side, flipped at a screen edge (default `'bottom'`)
|
|
781
|
+
/** Preferred side, flipped at a screen edge (default `'bottom'`);
|
|
782
|
+
* `'center'` goes over the anchor instead, and ignores `align`/`offset`. */
|
|
757
783
|
placement?: AnchorPlacement;
|
|
758
784
|
/** Which edges line up on the cross axis (default `'start'`). */
|
|
759
785
|
align?: 'start' | 'center' | 'end';
|
|
@@ -786,7 +812,7 @@ export interface AnchorOptions {
|
|
|
786
812
|
|
|
787
813
|
/** An anchored rect, and the side it actually ended up on. */
|
|
788
814
|
export interface AnchorRect extends Rect {
|
|
789
|
-
placement: Placement;
|
|
815
|
+
placement: Placement | 'center';
|
|
790
816
|
}
|
|
791
817
|
|
|
792
818
|
/**
|
|
@@ -820,7 +846,8 @@ export interface ScreenAnchorRect {
|
|
|
820
846
|
height?: number;
|
|
821
847
|
}
|
|
822
848
|
|
|
823
|
-
/** Centre a popup of this size
|
|
849
|
+
/** Centre a popup of this size over the window the node is in, clamped
|
|
850
|
+
* into the monitor — `placement: 'center'` against the window's rect. */
|
|
824
851
|
export function centerRect(
|
|
825
852
|
node: DrawnNode,
|
|
826
853
|
size: { width: number; height: number },
|
package/src/types/elements.d.ts
CHANGED
|
@@ -739,7 +739,11 @@ export interface TextInputProps extends DrawnProps<TextInputNode> {
|
|
|
739
739
|
* both read naturally.
|
|
740
740
|
*/
|
|
741
741
|
onChange?: (ev: ChangeEvent<TextInputNode>) => void;
|
|
742
|
-
/**
|
|
742
|
+
/**
|
|
743
|
+
* Enter or Shift+Enter — or Ctrl+Enter in a `<textarea>` (⌘↩ on the Cocoa
|
|
744
|
+
* backend). Any other chord on Enter is left to the shortcuts, and so is
|
|
745
|
+
* Enter itself in a field with no `onSubmit`.
|
|
746
|
+
*/
|
|
743
747
|
onSubmit?: (ev: SubmitEvent<TextInputNode>) => void;
|
|
744
748
|
placeholder?: string;
|
|
745
749
|
placeholderColor?: Color;
|
package/src/types/system.d.ts
CHANGED
|
@@ -169,7 +169,9 @@ export function useWindowState(
|
|
|
169
169
|
*
|
|
170
170
|
* Costs no timer where the X server carries an `IDLETIME` counter (Xorg
|
|
171
171
|
* does): a SYNC alarm fires on each crossing. Falls back to polling
|
|
172
|
-
* MIT-SCREEN-SAVER, and stays `false` on
|
|
172
|
+
* MIT-SCREEN-SAVER, and stays `false` on an X display with neither. On win32
|
|
173
|
+
* and macOS it polls the session's own idle time on the same adaptive
|
|
174
|
+
* schedule.
|
|
173
175
|
*/
|
|
174
176
|
export function useIdle(timeout: number): boolean;
|
|
175
177
|
|