react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/components/anchor.js
CHANGED
|
@@ -1,121 +1,153 @@
|
|
|
1
|
-
// Popup geometry:
|
|
2
|
-
//
|
|
1
|
+
// Popup geometry, the React half: the hooks a widget hangs a `<popup>` off
|
|
2
|
+
// a node with, and the measurement a menu sizes itself around.
|
|
3
|
+
//
|
|
4
|
+
// The placement math itself is core (`src/anchor.js`), because a `<popup>`
|
|
5
|
+
// that sizes itself from its content places itself from the same functions
|
|
6
|
+
// — see the note there.
|
|
3
7
|
|
|
4
|
-
import { useCallback } from 'react';
|
|
8
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export {
|
|
11
|
+
anchorArea,
|
|
12
|
+
anchorOffscreen,
|
|
13
|
+
anchorRect,
|
|
14
|
+
centerRect,
|
|
15
|
+
screenRect,
|
|
16
|
+
subRect,
|
|
17
|
+
} from '../anchor.js';
|
|
18
|
+
|
|
19
|
+
import { anchorOffscreen, anchorRect } from '../anchor.js';
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `placement` is a preference, not a promise — a menu near the bottom of
|
|
19
|
-
* the screen flips above its trigger rather than opening off-screen, and
|
|
20
|
-
* the result is clamped into the screen either way. The chosen side comes
|
|
21
|
-
* back as `placement` so the caller can style accordingly.
|
|
22
|
+
* useAnchor(ref) — stable `measure(options)` returning `anchorRect` for the
|
|
23
|
+
* referenced node. The anchoring math `Select` used to inline, shared with
|
|
24
|
+
* `Tooltip` and anything else that hangs a `<popup>` off a drawn node.
|
|
22
25
|
*/
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
placement = 'bottom',
|
|
27
|
-
align = 'start',
|
|
28
|
-
offset = 2,
|
|
29
|
-
width = node.abs.width,
|
|
30
|
-
height = 0,
|
|
31
|
-
} = options;
|
|
32
|
-
|
|
33
|
-
const win = node.root?.window;
|
|
34
|
-
const ax = (win?.x ?? 0) + node.abs.x;
|
|
35
|
-
const ay = (win?.y ?? 0) + node.abs.y;
|
|
36
|
-
const aw = node.abs.width;
|
|
37
|
-
const ah = node.abs.height;
|
|
38
|
-
|
|
39
|
-
const screen = screenOf(node);
|
|
40
|
-
const sw = screen?.pixel_width;
|
|
41
|
-
const sh = screen?.pixel_height;
|
|
42
|
-
|
|
43
|
-
const alignAlong = (start, size, extent) =>
|
|
44
|
-
align === 'center'
|
|
45
|
-
? start + (size - extent) / 2
|
|
46
|
-
: align === 'end'
|
|
47
|
-
? start + size - extent
|
|
48
|
-
: start;
|
|
49
|
-
|
|
50
|
-
let side = placement;
|
|
51
|
-
let x;
|
|
52
|
-
let y;
|
|
53
|
-
|
|
54
|
-
if (side === 'bottom' || side === 'top') {
|
|
55
|
-
const below = ay + ah + offset;
|
|
56
|
-
const above = ay - height - offset;
|
|
57
|
-
if (side === 'bottom' && sh != null && below + height > sh && above >= 0) {
|
|
58
|
-
side = 'top';
|
|
59
|
-
} else if (
|
|
60
|
-
side === 'top' &&
|
|
61
|
-
above < 0 &&
|
|
62
|
-
(sh == null || below + height <= sh)
|
|
63
|
-
) {
|
|
64
|
-
side = 'bottom';
|
|
65
|
-
}
|
|
66
|
-
y = side === 'bottom' ? below : above;
|
|
67
|
-
x = alignAlong(ax, aw, width);
|
|
68
|
-
} else {
|
|
69
|
-
const after = ax + aw + offset;
|
|
70
|
-
const before = ax - width - offset;
|
|
71
|
-
if (side === 'right' && sw != null && after + width > sw && before >= 0) {
|
|
72
|
-
side = 'left';
|
|
73
|
-
} else if (
|
|
74
|
-
side === 'left' &&
|
|
75
|
-
before < 0 &&
|
|
76
|
-
(sw == null || after + width <= sw)
|
|
77
|
-
) {
|
|
78
|
-
side = 'right';
|
|
79
|
-
}
|
|
80
|
-
x = side === 'right' ? after : before;
|
|
81
|
-
y = alignAlong(ay, ah, height);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (sw != null) x = Math.max(0, Math.min(x, sw - width));
|
|
85
|
-
if (sh != null && height) y = Math.max(0, Math.min(y, sh - height));
|
|
26
|
+
export function useAnchor(ref) {
|
|
27
|
+
return useCallback((options) => anchorRect(ref.current, options), [ref]);
|
|
28
|
+
}
|
|
86
29
|
|
|
87
|
-
|
|
30
|
+
function sameAnchorRect(a, b) {
|
|
31
|
+
return (
|
|
32
|
+
a === b ||
|
|
33
|
+
(a != null &&
|
|
34
|
+
b != null &&
|
|
35
|
+
a.x === b.x &&
|
|
36
|
+
a.y === b.y &&
|
|
37
|
+
a.width === b.width &&
|
|
38
|
+
a.height === b.height &&
|
|
39
|
+
a.placement === b.placement)
|
|
40
|
+
);
|
|
88
41
|
}
|
|
89
42
|
|
|
90
43
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
44
|
+
* useAnchorTracking(ref, active, getOptions, setRect, onOutOfView) — keeps a
|
|
45
|
+
* popup's `anchorRect` live for as long as `active`, instead of the one-shot
|
|
46
|
+
* measure every caller used to take at open time. A trigger inside a
|
|
47
|
+
* scrolled viewport, one whose own layout moves it (a neighbouring field
|
|
48
|
+
* wrapping to a second line), or an owner window nudged by the window
|
|
49
|
+
* manager or a script all leave a popup that was only ever measured once
|
|
50
|
+
* hanging over stale ground.
|
|
51
|
+
*
|
|
52
|
+
* Subscribes to the anchoring node's owner window (`WindowNode.onAnchorChange`,
|
|
53
|
+
* `nodes.js`) rather than polling: that fires exactly on the events which
|
|
54
|
+
* can actually move the rect — a layout pass and a fresh `_screenOrigin` —
|
|
55
|
+
* so a still trigger costs nothing and a moved one is caught the same frame.
|
|
56
|
+
* `getOptions` is read fresh through a ref on every notification, so callers
|
|
57
|
+
* do not need to memoize it; returning a falsy value skips that tick (the
|
|
58
|
+
* ref not being ready yet, say). `setRect` only runs when the measured rect
|
|
59
|
+
* actually differs, so it will not re-render (or re-`ConfigureWindow` the
|
|
60
|
+
* popup) for a change that turned out not to move anything.
|
|
61
|
+
*
|
|
62
|
+
* A popup is a real X window, not a web element clipped by its ancestors'
|
|
63
|
+
* overflow — following a trigger that has scrolled out of view would leave
|
|
64
|
+
* it floating over content it no longer belongs to, detached from anything
|
|
65
|
+
* the user can see it points at. So once the trigger is entirely past a
|
|
66
|
+
* clipping ancestor's own bounds or past the owner window itself
|
|
67
|
+
* (`Node._offscreen()`, the same check paint culling uses), tracking calls
|
|
68
|
+
* `onOutOfView` instead of measuring — closing the popup, or hiding it, is
|
|
69
|
+
* the caller's call — and does not resume until re-opened. With no
|
|
70
|
+
* `onOutOfView` this just stops updating rather than snapping to a rect
|
|
71
|
+
* that no longer means anything.
|
|
72
|
+
*
|
|
73
|
+
* With an `at` in the options that test is about **the sub-rect**, not the
|
|
74
|
+
* node: an editor scrolls its own text, so the caret leaves the viewport
|
|
75
|
+
* long before the editor does, and a completion popup that waited for the
|
|
76
|
+
* whole editor to scroll away would hang over the middle of a document it
|
|
77
|
+
* had stopped pointing into.
|
|
95
78
|
*/
|
|
96
|
-
export function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
export function useAnchorTracking(
|
|
80
|
+
ref,
|
|
81
|
+
active,
|
|
82
|
+
getOptions,
|
|
83
|
+
setRect,
|
|
84
|
+
onOutOfView,
|
|
85
|
+
) {
|
|
86
|
+
const measure = useAnchor(ref);
|
|
87
|
+
const getOptionsRef = useRef(getOptions);
|
|
88
|
+
getOptionsRef.current = getOptions;
|
|
89
|
+
const onOutOfViewRef = useRef(onOutOfView);
|
|
90
|
+
onOutOfViewRef.current = onOutOfView;
|
|
91
|
+
// Read during render, and read again inside the notification, because the
|
|
92
|
+
// two do not happen in that order. Closing a popup sets its rect to null
|
|
93
|
+
// and the *commit* destroys its window — and destroying a window is a
|
|
94
|
+
// layout pass, which notifies from inside the commit, before React gets
|
|
95
|
+
// to the effect cleanup that would have unsubscribed. A subscription that
|
|
96
|
+
// only knew the `active` of the render it was made in would answer that
|
|
97
|
+
// notification by measuring a fresh rect for the popup just closed, and
|
|
98
|
+
// hand back a second window in place of the one that went away.
|
|
99
|
+
const activeRef = useRef(active);
|
|
100
|
+
activeRef.current = active;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!active) return undefined;
|
|
104
|
+
const root = ref.current?.root;
|
|
105
|
+
if (!root?.onAnchorChange) return undefined;
|
|
106
|
+
return root.onAnchorChange(() => {
|
|
107
|
+
if (!activeRef.current) return;
|
|
108
|
+
const node = ref.current;
|
|
109
|
+
// the options before the view test, because `at` is in them: what has
|
|
110
|
+
// to still be visible is the rect the popup points at
|
|
111
|
+
const options = getOptionsRef.current();
|
|
112
|
+
if (!options) return;
|
|
113
|
+
if (anchorOffscreen(node, options.at)) {
|
|
114
|
+
onOutOfViewRef.current?.();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const next = measure(options);
|
|
118
|
+
if (!next) return;
|
|
119
|
+
setRect((prev) => (sameAnchorRect(prev, next) ? prev : next));
|
|
120
|
+
});
|
|
121
|
+
}, [active, ref, measure, setRect]);
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
125
|
+
* useDismissOnWindowBlur(ref, active, onDismiss) — shut a popup when the
|
|
126
|
+
* **window** it belongs to loses focus.
|
|
127
|
+
*
|
|
128
|
+
* A node's `onBlur` does not fire for this, deliberately: a window losing
|
|
129
|
+
* focus leaves the node inside it focused and merely stops it looking
|
|
130
|
+
* active, so the trigger a menu closes on never hears anything. What is left
|
|
131
|
+
* is a menu still open over an application the user has switched away from
|
|
132
|
+
* — and, for the ones that grab, still holding the pointer grab that came
|
|
133
|
+
* with it, so the first click anywhere goes to dismissing it.
|
|
134
|
+
*
|
|
135
|
+
* The window manager's own focus, then, rather than anything in the tree:
|
|
136
|
+
* `WindowNode.onWindowFocusChange` (nodes.js), which the event manager
|
|
137
|
+
* notifies from the same place it suspends the caret.
|
|
116
138
|
*/
|
|
117
|
-
export function
|
|
118
|
-
|
|
139
|
+
export function useDismissOnWindowBlur(ref, active, onDismiss) {
|
|
140
|
+
const onDismissRef = useRef(onDismiss);
|
|
141
|
+
onDismissRef.current = onDismiss;
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!active) return undefined;
|
|
145
|
+
const root = ref.current?.root;
|
|
146
|
+
if (!root?.onWindowFocusChange) return undefined;
|
|
147
|
+
return root.onWindowFocusChange((focused) => {
|
|
148
|
+
if (!focused) onDismissRef.current?.();
|
|
149
|
+
});
|
|
150
|
+
}, [active, ref]);
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
/** Measured size of a single-line label, for sizing a popup around it.
|
|
@@ -126,12 +158,27 @@ export function measureLabel(node, text, style) {
|
|
|
126
158
|
if (!fonts?.layout) {
|
|
127
159
|
return { width: String(text).length * size * 0.55, height: size * 1.4 };
|
|
128
160
|
}
|
|
161
|
+
// Measured at the size the glyphs are actually rasterized — the logical
|
|
162
|
+
// size times the display scale — then divided back, so the logical answer
|
|
163
|
+
// carries the true metrics. Measuring at the logical size and trusting
|
|
164
|
+
// linearity is off by the accumulated per-glyph rounding, which is
|
|
165
|
+
// exactly the kind of error that turns into an ellipsis on the longest
|
|
166
|
+
// label in the menu (src/scale.js).
|
|
167
|
+
const s = node?.scale ?? 1;
|
|
129
168
|
const layout = fonts.layout(String(text), {
|
|
130
|
-
|
|
131
|
-
|
|
169
|
+
// The face **this node inherits**, not the literal `sans-serif`: the
|
|
170
|
+
// labels these popups are sized around name no family of their own, so
|
|
171
|
+
// the one they are drawn in is the palette's. Measuring in a different
|
|
172
|
+
// face is measuring the wrong label — a menu sized in sans-serif for a
|
|
173
|
+
// row that paints in a wider mono is a menu whose own options wrap.
|
|
174
|
+
family: style?.family ?? node?.inheritedTextStyle?.family ?? 'sans-serif',
|
|
175
|
+
size: size * s,
|
|
132
176
|
weight: style?.weight ?? 'normal',
|
|
177
|
+
// dropping this would measure a different face from the one drawn, and
|
|
178
|
+
// the popup sized here would be the wrong width for its own label
|
|
179
|
+
variations: style?.variations,
|
|
133
180
|
});
|
|
134
|
-
return { width: layout.width, height: layout.height };
|
|
181
|
+
return { width: layout.width / s, height: layout.height / s };
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
export const DEFAULT_LABEL_SIZE = 13;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The change event the value widgets hand their `onChange`.
|
|
2
|
+
//
|
|
3
|
+
// One signature across the library: `<textinput>` and `<textarea>` pass a
|
|
4
|
+
// synthetic event, and so do `Checkbox`, `Switch`, `RadioGroup`, `Select`
|
|
5
|
+
// and `Slider`. That is what lets `onChange={formik.handleChange}` be wired
|
|
6
|
+
// to any of them without a per-widget adapter — a form library reads the
|
|
7
|
+
// field out of `ev.target`, and it now finds one there whatever it is
|
|
8
|
+
// attached to.
|
|
9
|
+
//
|
|
10
|
+
// The line is `name`: a widget that takes one is a form field and reports an
|
|
11
|
+
// event. `Tabs`, `Table` and the menus are not form fields and keep
|
|
12
|
+
// their plain callbacks.
|
|
13
|
+
//
|
|
14
|
+
// `target` is a plain descriptor rather than a node, which is the one place
|
|
15
|
+
// this differs from the host elements. A widget is a composition of nodes
|
|
16
|
+
// with no single element holding its value, so there is nothing honest to
|
|
17
|
+
// point at — and `{ type, name, value, checked }` is exactly the shape
|
|
18
|
+
// formik's `handleChange` and react-hook-form's event reader destructure.
|
|
19
|
+
// `type` is what tells them a checkbox from a text field, which is why it is
|
|
20
|
+
// set even though nothing in react-x11 reads it.
|
|
21
|
+
//
|
|
22
|
+
// There is no `preventDefault` — the value has already changed by the time
|
|
23
|
+
// the handler runs, so there would be nothing to prevent.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} type - `'checkbox'`, `'radio'`, `'select-one'`, `'range'`…
|
|
27
|
+
* @param {string|undefined} name - the widget's `name` prop
|
|
28
|
+
* @param {unknown} value - the value the widget just moved to
|
|
29
|
+
*/
|
|
30
|
+
export function changeEvent(type, name, value) {
|
|
31
|
+
const target = { type, name, value };
|
|
32
|
+
if (type === 'checkbox') target.checked = Boolean(value);
|
|
33
|
+
return { type: 'change', target, currentTarget: target, name, value };
|
|
34
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// useDropTarget — the convenience layer over the drop-target host props
|
|
2
|
+
// (`dropAccept`, `onDragEnter/Leave/Over`, `onDrop`; see src/dnd.js), the
|
|
3
|
+
// same two-layer shape as anchorRect/useAnchor. The hook adds what a
|
|
4
|
+
// component wants and the host props deliberately do not: render state.
|
|
5
|
+
// The common "highlight the dropzone" case needs neither — a
|
|
6
|
+
// `':drag-over'` style block does it with no state at all — so reach for
|
|
7
|
+
// this only when the render itself changes (an "isOver" hint label,
|
|
8
|
+
// disabling other controls mid-drag).
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { matchAccept } from '../dnd.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* const { dropProps, isOver, isAccepted } = useDropTarget({
|
|
15
|
+
* accept: ['files'],
|
|
16
|
+
* onDrop: (e) => setFiles(e.files),
|
|
17
|
+
* });
|
|
18
|
+
* return <box {...dropProps} style={s.zone} />;
|
|
19
|
+
*
|
|
20
|
+
* `accept` here maps to the `dropAccept` host prop — inside a hook named
|
|
21
|
+
* useDropTarget the shorter word is unambiguous, and it is the name
|
|
22
|
+
* react-dropzone users expect. `onDrop`'s return value is passed through,
|
|
23
|
+
* so an async handler keeps holding XdndFinished (up to the watchdog).
|
|
24
|
+
*/
|
|
25
|
+
export function useDropTarget(options = {}) {
|
|
26
|
+
const { accept, onDrop, onDragOver, onDragEnter, onDragLeave } = options;
|
|
27
|
+
// null: no drag over this node; otherwise whether the offer matches
|
|
28
|
+
const [drag, setDrag] = useState(null);
|
|
29
|
+
const dropProps = {
|
|
30
|
+
dropAccept: accept,
|
|
31
|
+
onDragEnter: (ev) => {
|
|
32
|
+
setDrag({ accepted: matchAccept(accept ?? null, ev.types) });
|
|
33
|
+
onDragEnter?.(ev);
|
|
34
|
+
},
|
|
35
|
+
onDragLeave: (ev) => {
|
|
36
|
+
setDrag(null);
|
|
37
|
+
onDragLeave?.(ev);
|
|
38
|
+
},
|
|
39
|
+
onDragOver,
|
|
40
|
+
onDrop: (ev) => {
|
|
41
|
+
setDrag(null);
|
|
42
|
+
return onDrop?.(ev);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
dropProps,
|
|
47
|
+
isOver: drag != null,
|
|
48
|
+
isAccepted: Boolean(drag?.accepted),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* useDragSource — the source-side twin. Spread `dragProps` on the node to
|
|
54
|
+
* drag; the hook adds render state (`isDragging`, the pointer `position` in
|
|
55
|
+
* screen coordinates) so a component can dim itself, show a hint, or render
|
|
56
|
+
* a drag preview. The preview is deliberately userland — a `<popup
|
|
57
|
+
* dragPreview>` following `position` is a live React tree, which is more
|
|
58
|
+
* than a DOM `setDragImage` bitmap ever was; the `dragPreview` prop is what
|
|
59
|
+
* keeps the router from seeing the popup as the window under the pointer.
|
|
60
|
+
*
|
|
61
|
+
* const { dragProps, isDragging, position } = useDragSource({
|
|
62
|
+
* data: {
|
|
63
|
+
* 'text/uri-list': () => toUriList(item), // thunk: resolved lazily
|
|
64
|
+
* 'application/x-myapp-item': item, // live for in-app drops
|
|
65
|
+
* },
|
|
66
|
+
* actions: ['copy', 'move'],
|
|
67
|
+
* onDragEnd: (e) => { if (e.action === 'move') remove(item.id); },
|
|
68
|
+
* });
|
|
69
|
+
* return (
|
|
70
|
+
* <>
|
|
71
|
+
* <box {...dragProps} style={{ ':dragging': { backgroundColor: '$textMuted' } }} />
|
|
72
|
+
* {isDragging && (
|
|
73
|
+
* <popup dragPreview x={position.x + 12} y={position.y + 12}>
|
|
74
|
+
* <Chip label={item.name} />
|
|
75
|
+
* </popup>
|
|
76
|
+
* )}
|
|
77
|
+
* </>
|
|
78
|
+
* );
|
|
79
|
+
*/
|
|
80
|
+
export function useDragSource(options = {}) {
|
|
81
|
+
const { data, actions, onDragStart, onDrag, onDragEnd } = options;
|
|
82
|
+
const [drag, setDrag] = useState(null);
|
|
83
|
+
const dragProps = {
|
|
84
|
+
draggable: true,
|
|
85
|
+
dragData: data,
|
|
86
|
+
dragActions: actions,
|
|
87
|
+
onDragStart: (ev) => {
|
|
88
|
+
onDragStart?.(ev);
|
|
89
|
+
if (!ev.defaultPrevented) {
|
|
90
|
+
setDrag({ x: ev.screenX, y: ev.screenY, accepted: false });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
onDrag: (ev) => {
|
|
94
|
+
setDrag({ x: ev.screenX, y: ev.screenY, accepted: ev.accepted });
|
|
95
|
+
onDrag?.(ev);
|
|
96
|
+
},
|
|
97
|
+
onDragEnd: (ev) => {
|
|
98
|
+
setDrag(null);
|
|
99
|
+
onDragEnd?.(ev);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
return { dragProps, isDragging: drag != null, position: drag };
|
|
103
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
// modules here are grouped by widget, with the shared plumbing in
|
|
3
3
|
// theme.js (palette + control props), anchor.js (popup geometry),
|
|
4
4
|
// typeahead.js and keys.js.
|
|
5
|
-
export { ThemeProvider,
|
|
6
|
-
export {
|
|
5
|
+
export { ThemeProvider, useDirection, useTheme } from './theme.js';
|
|
6
|
+
export {
|
|
7
|
+
anchorArea,
|
|
8
|
+
anchorRect,
|
|
9
|
+
centerRect,
|
|
10
|
+
screenRect,
|
|
11
|
+
useAnchor,
|
|
12
|
+
useAnchorTracking,
|
|
13
|
+
} from './anchor.js';
|
|
14
|
+
export { useDropTarget, useDragSource } from './dnd.js';
|
|
15
|
+
export { Icon, icons, iconNames, iconSize } from './Icon.js';
|
|
7
16
|
export { Button } from './Button.js';
|
|
17
|
+
export { PasswordInput } from './PasswordInput.js';
|
|
8
18
|
export { Dialog } from './Dialog.js';
|
|
19
|
+
export { FileDialog } from './FileDialog.js';
|
|
9
20
|
export { Checkbox } from './Checkbox.js';
|
|
10
21
|
export { Radio, RadioGroup } from './Radio.js';
|
|
11
22
|
export { Switch } from './Switch.js';
|
|
@@ -14,4 +25,6 @@ export { Slider } from './Slider.js';
|
|
|
14
25
|
export { Tooltip } from './Tooltip.js';
|
|
15
26
|
export { ContextMenu, MenuBar } from './Menu.js';
|
|
16
27
|
export { Select } from './Select.js';
|
|
17
|
-
export {
|
|
28
|
+
export { Tabs } from './Tabs.js';
|
|
29
|
+
export { Table } from './Table.js';
|
|
30
|
+
export { SplitPane } from './SplitPane.js';
|
package/src/components/keys.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
// X11 keysyms used by the widget keyboard handlers.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const XK_PAGE_DOWN = 0xff56;
|
|
20
|
-
|
|
21
|
-
export const XK_END = 0xff57;
|
|
2
|
+
//
|
|
3
|
+
// The names live in `src/keysyms.js`, published as `react-x11/keysyms`, so
|
|
4
|
+
// app code, the widgets and `fireEvent.key` in `react-x11/test` all name a
|
|
5
|
+
// key the same way. This module stays as the widgets' import path.
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
XK_RETURN,
|
|
9
|
+
XK_ESCAPE,
|
|
10
|
+
XK_HOME,
|
|
11
|
+
XK_LEFT,
|
|
12
|
+
XK_UP,
|
|
13
|
+
XK_RIGHT,
|
|
14
|
+
XK_DOWN,
|
|
15
|
+
XK_PAGE_UP,
|
|
16
|
+
XK_PAGE_DOWN,
|
|
17
|
+
XK_END,
|
|
18
|
+
} from '../keysyms.js';
|