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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -1,121 +1,153 @@
1
- // Popup geometry: where to put a <popup> anchored to a drawn node, and
2
- // how big to make it around a measured label.
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
- /** The X screen the node's window lives on, if reachable (the smoke-test
7
- * mock app has no screen geometry — callers must cope with null). */
8
- export function screenOf(node) {
9
- const app = node?.app;
10
- const screen = (app?.display ?? app?.X?.display)?.screen?.[0];
11
- return screen?.pixel_width ? screen : null;
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
- * Where to put a `<popup>` anchored to a drawn node, in **screen**
16
- * coordinates: the owner window's position plus the node's laid-out rect.
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 anchorRect(node, options = {}) {
24
- if (!node?.abs) return null;
25
- const {
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
- return { x: Math.round(x), y: Math.round(y), width, height, placement: side };
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
- * Where to put a `<popup>` of this size **centred over the owner window**,
92
- * in screen coordinates, clamped into the screen. A dialog is anchored to
93
- * the window rather than to a widget, which is the one placement
94
- * `anchorRect` cannot express.
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 centerRect(node, { width, height }) {
97
- if (!node) return null;
98
- const win = node.root?.window;
99
- const ww = win?.width ?? width;
100
- const wh = win?.height ?? height;
101
- let x = (win?.x ?? 0) + (ww - width) / 2;
102
- let y = (win?.y ?? 0) + (wh - height) / 2;
103
-
104
- const screen = screenOf(node);
105
- if (screen) {
106
- x = Math.max(0, Math.min(x, screen.pixel_width - width));
107
- y = Math.max(0, Math.min(y, screen.pixel_height - height));
108
- }
109
- return { x: Math.round(x), y: Math.round(y), width, height };
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
- * useAnchor(ref) — stable `measure(options)` returning `anchorRect` for the
114
- * referenced node. The anchoring math `Select` used to inline, shared with
115
- * `Tooltip` and anything else that hangs a `<popup>` off a drawn node.
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 useAnchor(ref) {
118
- return useCallback((options) => anchorRect(ref.current, options), [ref]);
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
- family: style?.family ?? 'sans-serif',
131
- size,
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
+ }
@@ -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, SelectThemeProvider } from './theme.js';
6
- export { anchorRect, centerRect, useAnchor } from './anchor.js';
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 { Canvas3D } from './Canvas3D.js';
28
+ export { Tabs } from './Tabs.js';
29
+ export { Table } from './Table.js';
30
+ export { SplitPane } from './SplitPane.js';
@@ -1,21 +1,18 @@
1
1
  // X11 keysyms used by the widget keyboard handlers.
2
-
3
- export const XK_RETURN = 0xff0d;
4
-
5
- export const XK_ESCAPE = 0xff1b;
6
-
7
- export const XK_HOME = 0xff50;
8
-
9
- export const XK_LEFT = 0xff51;
10
-
11
- export const XK_UP = 0xff52;
12
-
13
- export const XK_RIGHT = 0xff53;
14
-
15
- export const XK_DOWN = 0xff54;
16
-
17
- export const XK_PAGE_UP = 0xff55;
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';