react-x11 0.0.1 → 1.2.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/LICENSE +21 -0
- package/README.md +226 -0
- package/package.json +60 -7
- package/src/ClickToComponent.js +179 -0
- package/src/DevToolsIntegration.js +106 -0
- package/src/Reconciler.js +507 -0
- package/src/components/Button.js +63 -0
- package/src/components/Canvas3D.js +28 -0
- package/src/components/Checkbox.js +69 -0
- package/src/components/Dialog.js +138 -0
- package/src/components/Menu.js +644 -0
- package/src/components/ProgressBar.js +48 -0
- package/src/components/Radio.js +95 -0
- package/src/components/Select.js +272 -0
- package/src/components/Slider.js +177 -0
- package/src/components/Switch.js +49 -0
- package/src/components/Tooltip.js +146 -0
- package/src/components/anchor.js +211 -0
- package/src/components/index.js +17 -0
- package/src/components/keys.js +21 -0
- package/src/components/theme.js +66 -0
- package/src/components/typeahead.js +56 -0
- package/src/events.js +584 -0
- package/src/geometry3d.js +223 -0
- package/src/glnodes.js +275 -0
- package/src/index.js +30 -0
- package/src/mat4.js +235 -0
- package/src/nodes.js +1985 -0
- package/src/pointer3d.js +158 -0
- package/src/priority.js +39 -0
- package/src/raycast3d.js +146 -0
- package/src/richnodes.js +436 -0
- package/src/scene3d.js +683 -0
- package/src/styles.js +189 -0
- package/.npmignore +0 -17
- package/combobox.jsx +0 -69
- package/react-x11.js +0 -119
- package/test-canvas.js +0 -98
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// The react-reconciler host config plus the public render entry points.
|
|
2
|
+
// Host instances are the retained nodes from nodes.js; only <window> and
|
|
3
|
+
// <popup> map to real X11 windows (see NEXT_STEPS.md), and those windows
|
|
4
|
+
// are created top-down in the commit phase (WindowNode.realize) so every
|
|
5
|
+
// CreateWindow names its actual parent from the start — createInstance
|
|
6
|
+
// performs no X11 calls, since the render phase is discardable (issue #4).
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import ReactReconciler from 'react-reconciler';
|
|
10
|
+
import { createClient } from 'ntk';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ConcurrentRoot,
|
|
14
|
+
getCurrentUpdatePriority,
|
|
15
|
+
setCurrentUpdatePriority,
|
|
16
|
+
resolveUpdatePriority,
|
|
17
|
+
} from './priority.js';
|
|
18
|
+
import {
|
|
19
|
+
WindowNode,
|
|
20
|
+
PopupNode,
|
|
21
|
+
BoxNode,
|
|
22
|
+
TextNode,
|
|
23
|
+
TextChunkNode,
|
|
24
|
+
ImageNode,
|
|
25
|
+
CanvasNode,
|
|
26
|
+
ScrollViewNode,
|
|
27
|
+
TextInputNode,
|
|
28
|
+
TextAreaNode,
|
|
29
|
+
flushWindowRestacks,
|
|
30
|
+
} from './nodes.js';
|
|
31
|
+
import { GlAreaNode } from './glnodes.js';
|
|
32
|
+
import { SCENE_KINDS, UNSUPPORTED_KINDS, createSceneNode } from './scene3d.js';
|
|
33
|
+
import {
|
|
34
|
+
MarkdownNode,
|
|
35
|
+
HtmlNode,
|
|
36
|
+
SvgNode,
|
|
37
|
+
SvgChildNode,
|
|
38
|
+
TexNode,
|
|
39
|
+
} from './richnodes.js';
|
|
40
|
+
|
|
41
|
+
const require = createRequire(import.meta.url);
|
|
42
|
+
const packageJson = require('../package.json');
|
|
43
|
+
|
|
44
|
+
const HOST_TYPES = [
|
|
45
|
+
'window',
|
|
46
|
+
'popup',
|
|
47
|
+
'box',
|
|
48
|
+
'text',
|
|
49
|
+
'image',
|
|
50
|
+
'canvas',
|
|
51
|
+
'scrollview',
|
|
52
|
+
'textinput',
|
|
53
|
+
'textarea',
|
|
54
|
+
'markdown',
|
|
55
|
+
'html',
|
|
56
|
+
'svg',
|
|
57
|
+
'tex',
|
|
58
|
+
'glarea',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const isEventProp = (name) => /^on[A-Z]/.test(name);
|
|
62
|
+
|
|
63
|
+
// Props forwarded to ntk createWindow. Event handlers are dispatched by the
|
|
64
|
+
// EventManager from current props (never registered at creation, so they
|
|
65
|
+
// cannot go stale) and children are handled by the tree.
|
|
66
|
+
function windowAttributes(props) {
|
|
67
|
+
const attributes = {};
|
|
68
|
+
for (const key of Object.keys(props)) {
|
|
69
|
+
if (key === 'children' || isEventProp(key)) continue;
|
|
70
|
+
attributes[key] = props[key];
|
|
71
|
+
}
|
|
72
|
+
return attributes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const HostConfig = {
|
|
76
|
+
supportsMutation: true,
|
|
77
|
+
supportsPersistence: false,
|
|
78
|
+
supportsHydration: false,
|
|
79
|
+
supportsResources: false,
|
|
80
|
+
supportsSingletons: false,
|
|
81
|
+
supportsTestSelectors: false,
|
|
82
|
+
supportsMicrotasks: true,
|
|
83
|
+
isPrimaryRenderer: true,
|
|
84
|
+
warnsIfNotActing: false,
|
|
85
|
+
|
|
86
|
+
rendererVersion: packageJson.version,
|
|
87
|
+
rendererPackageName: packageJson.name,
|
|
88
|
+
extraDevToolsConfig: null,
|
|
89
|
+
|
|
90
|
+
scheduleTimeout: setTimeout,
|
|
91
|
+
cancelTimeout: clearTimeout,
|
|
92
|
+
noTimeout: -1,
|
|
93
|
+
scheduleMicrotask: queueMicrotask,
|
|
94
|
+
|
|
95
|
+
getRootHostContext() {
|
|
96
|
+
return {
|
|
97
|
+
isInsideText: false,
|
|
98
|
+
isInsideSvg: false,
|
|
99
|
+
isInsideRichText: false,
|
|
100
|
+
isInside3d: false,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
getChildHostContext(parentHostContext, type) {
|
|
105
|
+
return {
|
|
106
|
+
isInsideText: parentHostContext.isInsideText || type === 'text',
|
|
107
|
+
// <svg> children are declarative SVG elements, not react-x11 nodes
|
|
108
|
+
isInsideSvg: parentHostContext.isInsideSvg || type === 'svg',
|
|
109
|
+
// <markdown>/<html>/<tex> take their content as a string child
|
|
110
|
+
// (react-markdown style); no elements are allowed inside
|
|
111
|
+
isInsideRichText:
|
|
112
|
+
type === 'markdown' || type === 'html' || type === 'tex',
|
|
113
|
+
// inside <glarea> the children are scene nodes, not drawn nodes
|
|
114
|
+
isInside3d: parentHostContext.isInside3d || type === 'glarea',
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
getPublicInstance(instance) {
|
|
119
|
+
// Refs attach in the layout phase, after the mutation phase realized
|
|
120
|
+
// the window, so for windows this is the live ntk window.
|
|
121
|
+
return instance.isWindow ? instance.window || instance : instance;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
prepareForCommit() {
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// child <window>s that moved in the tree restack here, so a reorder costs
|
|
129
|
+
// one pass instead of one per insertBefore
|
|
130
|
+
resetAfterCommit() {
|
|
131
|
+
flushWindowRestacks();
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
createInstance(type, props, rootContainer, hostContext, internalHandle) {
|
|
135
|
+
if (hostContext.isInsideSvg) {
|
|
136
|
+
// Inside <svg> every element is a declarative SVG element (React-DOM
|
|
137
|
+
// style: <circle cx={12} strokeWidth={2} />); SvgView skips tags it
|
|
138
|
+
// does not support.
|
|
139
|
+
const node = new SvgChildNode(type, props, rootContainer);
|
|
140
|
+
node._reactFiber = internalHandle;
|
|
141
|
+
return node;
|
|
142
|
+
}
|
|
143
|
+
if (hostContext.isInsideRichText) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`react-x11: <${type}> is not allowed inside <markdown>/<html>/<tex>; ` +
|
|
146
|
+
'their content is a string child (or the source prop).',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (hostContext.isInsideText && type !== 'text') {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`react-x11: <${type}> is not allowed inside <text>; only nested ` +
|
|
152
|
+
'<text> spans and strings are.',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (hostContext.isInside3d) {
|
|
156
|
+
const scene = createSceneNode(type, props, rootContainer);
|
|
157
|
+
if (scene) {
|
|
158
|
+
scene._reactFiber = internalHandle;
|
|
159
|
+
return scene;
|
|
160
|
+
}
|
|
161
|
+
if (UNSUPPORTED_KINDS[type]) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`react-x11: <${type}> cannot work over indirect GLX — ` +
|
|
164
|
+
`${UNSUPPORTED_KINDS[type]}. See docs/glx-plan.md.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
throw new Error(
|
|
168
|
+
`react-x11: <${type}> is not a 3D element; inside <glarea> only ` +
|
|
169
|
+
[...SCENE_KINDS].map((t) => `<${t}>`).join(', ') +
|
|
170
|
+
' are.',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
let node;
|
|
174
|
+
switch (type) {
|
|
175
|
+
case 'window':
|
|
176
|
+
// No X11 calls here: the render phase may be discarded. The real
|
|
177
|
+
// window is created top-down in the commit phase (realize).
|
|
178
|
+
node = new WindowNode(rootContainer, windowAttributes(props), props);
|
|
179
|
+
break;
|
|
180
|
+
case 'popup':
|
|
181
|
+
node = new PopupNode(rootContainer, windowAttributes(props), props);
|
|
182
|
+
break;
|
|
183
|
+
case 'box':
|
|
184
|
+
node = new BoxNode(props, rootContainer);
|
|
185
|
+
break;
|
|
186
|
+
case 'scrollview':
|
|
187
|
+
node = new ScrollViewNode(props, rootContainer);
|
|
188
|
+
break;
|
|
189
|
+
case 'textinput':
|
|
190
|
+
node = new TextInputNode(props, rootContainer);
|
|
191
|
+
break;
|
|
192
|
+
case 'textarea':
|
|
193
|
+
node = new TextAreaNode(props, rootContainer);
|
|
194
|
+
break;
|
|
195
|
+
case 'text':
|
|
196
|
+
node = new TextNode(props, rootContainer, {
|
|
197
|
+
span: hostContext.isInsideText,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
case 'image':
|
|
201
|
+
node = new ImageNode(props, rootContainer);
|
|
202
|
+
break;
|
|
203
|
+
case 'canvas':
|
|
204
|
+
node = new CanvasNode(props, rootContainer);
|
|
205
|
+
break;
|
|
206
|
+
case 'markdown':
|
|
207
|
+
node = new MarkdownNode(props, rootContainer);
|
|
208
|
+
break;
|
|
209
|
+
case 'html':
|
|
210
|
+
node = new HtmlNode(props, rootContainer);
|
|
211
|
+
break;
|
|
212
|
+
case 'svg':
|
|
213
|
+
node = new SvgNode(props, rootContainer);
|
|
214
|
+
break;
|
|
215
|
+
case 'tex':
|
|
216
|
+
node = new TexNode(props, rootContainer);
|
|
217
|
+
break;
|
|
218
|
+
case 'glarea':
|
|
219
|
+
node = new GlAreaNode(props, rootContainer);
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
if (SCENE_KINDS.has(type) || UNSUPPORTED_KINDS[type]) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`react-x11: <${type}> is a 3D element and only works inside ` +
|
|
225
|
+
'<glarea> (or the <Canvas3D> component).',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw new Error(
|
|
229
|
+
`react-x11: unknown element type <${type}>. Supported: ` +
|
|
230
|
+
HOST_TYPES.map((t) => `<${t}>`).join(', ') +
|
|
231
|
+
'.',
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
node._reactFiber = internalHandle;
|
|
235
|
+
return node;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
createTextInstance(text, rootContainer, hostContext) {
|
|
239
|
+
if (
|
|
240
|
+
!hostContext.isInsideText &&
|
|
241
|
+
!hostContext.isInsideRichText &&
|
|
242
|
+
!hostContext.isInsideSvg
|
|
243
|
+
) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`react-x11: raw text ${JSON.stringify(text)} must be wrapped in a ` +
|
|
246
|
+
'<text> element (or be the string child of <markdown>/<html>/' +
|
|
247
|
+
'<tex>/an SVG <text>).',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return new TextChunkNode(text, rootContainer);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
appendInitialChild(parentInstance, child) {
|
|
254
|
+
parentInstance.insertBefore(child, null);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
finalizeInitialChildren(instance, type, props) {
|
|
258
|
+
// Popups are not attached to the container or realized by a parent
|
|
259
|
+
// window; commitMount realizes them against the screen root. autoFocus
|
|
260
|
+
// and trapFocus need commitMount too — the node has to be in the tree
|
|
261
|
+
// first, so it can find the EventManager that owns focus.
|
|
262
|
+
return (
|
|
263
|
+
type === 'popup' || Boolean(props.autoFocus) || Boolean(props.trapFocus)
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
commitMount(instance, type, props) {
|
|
268
|
+
if (type === 'popup') {
|
|
269
|
+
instance.realize(null);
|
|
270
|
+
}
|
|
271
|
+
if (props.trapFocus) {
|
|
272
|
+
instance._syncFocusScope?.();
|
|
273
|
+
}
|
|
274
|
+
if (props.autoFocus && typeof instance.focus === 'function') {
|
|
275
|
+
instance.focus();
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
appendChild(parentInstance, child) {
|
|
280
|
+
parentInstance.insertBefore(child, null);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
appendChildToContainer(container, child) {
|
|
284
|
+
if (!child.window) {
|
|
285
|
+
// Top-level window: realize the whole subtree top-down against the
|
|
286
|
+
// screen root.
|
|
287
|
+
child.realize(null);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
insertBefore(parentInstance, child, beforeChild) {
|
|
292
|
+
parentInstance.insertBefore(child, beforeChild);
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
insertInContainerBefore(container, child) {
|
|
296
|
+
HostConfig.appendChildToContainer(container, child);
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
removeChild(parentInstance, child) {
|
|
300
|
+
parentInstance.removeChild(child);
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
removeChildFromContainer(container, child) {
|
|
304
|
+
child.destroySubtree();
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
clearContainer() {},
|
|
308
|
+
|
|
309
|
+
commitUpdate(instance, type, oldProps, newProps) {
|
|
310
|
+
instance.applyProps(newProps, oldProps);
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
shouldSetTextContent() {
|
|
314
|
+
return false;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
commitTextUpdate(textInstance, oldText, newText) {
|
|
318
|
+
textInstance.setText(newText);
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
resetTextContent() {},
|
|
322
|
+
|
|
323
|
+
hideInstance(instance) {
|
|
324
|
+
instance.setHidden(true);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
unhideInstance(instance) {
|
|
328
|
+
instance.setHidden(false);
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
hideTextInstance(textInstance) {
|
|
332
|
+
textInstance.setText('');
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
unhideTextInstance(textInstance, text) {
|
|
336
|
+
textInstance.setText(text);
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
detachDeletedInstance(instance) {
|
|
340
|
+
instance.root?.events?.forget(instance);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
preparePortalMount() {},
|
|
344
|
+
prepareScopeUpdate() {},
|
|
345
|
+
getInstanceFromScope() {
|
|
346
|
+
return null;
|
|
347
|
+
},
|
|
348
|
+
getInstanceFromNode() {
|
|
349
|
+
return null;
|
|
350
|
+
},
|
|
351
|
+
beforeActiveInstanceBlur() {},
|
|
352
|
+
afterActiveInstanceBlur() {},
|
|
353
|
+
|
|
354
|
+
// Update priority plumbing (React 19 scheduling contract).
|
|
355
|
+
setCurrentUpdatePriority,
|
|
356
|
+
getCurrentUpdatePriority,
|
|
357
|
+
resolveUpdatePriority,
|
|
358
|
+
shouldAttemptEagerTransition() {
|
|
359
|
+
return false;
|
|
360
|
+
},
|
|
361
|
+
trackSchedulerEvent() {},
|
|
362
|
+
resolveEventType() {
|
|
363
|
+
return null;
|
|
364
|
+
},
|
|
365
|
+
resolveEventTimeStamp() {
|
|
366
|
+
return -1.1;
|
|
367
|
+
},
|
|
368
|
+
requestPostPaintCallback() {},
|
|
369
|
+
|
|
370
|
+
// Suspensey commits are not used by this renderer.
|
|
371
|
+
maySuspendCommit() {
|
|
372
|
+
return false;
|
|
373
|
+
},
|
|
374
|
+
maySuspendCommitOnUpdate() {
|
|
375
|
+
return false;
|
|
376
|
+
},
|
|
377
|
+
maySuspendCommitInSyncRender() {
|
|
378
|
+
return false;
|
|
379
|
+
},
|
|
380
|
+
preloadInstance() {
|
|
381
|
+
return true;
|
|
382
|
+
},
|
|
383
|
+
startSuspendingCommit() {},
|
|
384
|
+
suspendInstance() {},
|
|
385
|
+
waitForCommitToBeReady() {
|
|
386
|
+
return null;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// Form actions / transitions (unused, required by the contract).
|
|
390
|
+
NotPendingTransition: null,
|
|
391
|
+
HostTransitionContext: React.createContext(null),
|
|
392
|
+
resetFormInstance() {},
|
|
393
|
+
|
|
394
|
+
bindToConsole(methodName, args) {
|
|
395
|
+
return Function.prototype.bind.apply(console[methodName], [
|
|
396
|
+
console,
|
|
397
|
+
...args,
|
|
398
|
+
]);
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export const Renderer = ReactReconciler(HostConfig);
|
|
403
|
+
|
|
404
|
+
if (process.env.REACT_X11_DEVTOOLS) {
|
|
405
|
+
// Install the DevTools hook before any React commit (top-level await:
|
|
406
|
+
// module evaluation finishes before app code can call render) and
|
|
407
|
+
// register the renderer with the standalone DevTools app.
|
|
408
|
+
const devtools = await import('./DevToolsIntegration.js');
|
|
409
|
+
await devtools.prepare();
|
|
410
|
+
devtools.connect(Renderer);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (process.env.REACT_X11_CLICK_TO_COMPONENT || process.env.REACT_X11_EDITOR) {
|
|
414
|
+
// Naming an editor already means you want the feature on — no need to
|
|
415
|
+
// also set REACT_X11_CLICK_TO_COMPONENT=1 just to pick one.
|
|
416
|
+
const clickToComponent = await import('./ClickToComponent.js');
|
|
417
|
+
clickToComponent.install();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const roots = new Map();
|
|
421
|
+
let cachedNtkApp = null;
|
|
422
|
+
|
|
423
|
+
async function connectApp() {
|
|
424
|
+
if (cachedNtkApp) return cachedNtkApp;
|
|
425
|
+
try {
|
|
426
|
+
cachedNtkApp = await createClient();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
'react-x11: could not connect to the X server. Is an X server running ' +
|
|
430
|
+
`and DISPLAY set (DISPLAY=${process.env.DISPLAY || '<unset>'})? ` +
|
|
431
|
+
'Original error: ' +
|
|
432
|
+
err.message,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return cachedNtkApp;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function renderIntoContainer(element, container, callback) {
|
|
439
|
+
let root = roots.get(container);
|
|
440
|
+
if (!root) {
|
|
441
|
+
root = Renderer.createContainer(
|
|
442
|
+
container,
|
|
443
|
+
ConcurrentRoot,
|
|
444
|
+
null,
|
|
445
|
+
false,
|
|
446
|
+
null,
|
|
447
|
+
'',
|
|
448
|
+
(error) => console.error('react-x11: uncaught error', error),
|
|
449
|
+
(error) => console.error('react-x11: caught error', error),
|
|
450
|
+
(error) => console.error('react-x11: recoverable error', error),
|
|
451
|
+
null,
|
|
452
|
+
);
|
|
453
|
+
roots.set(container, root);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
Renderer.updateContainerSync(element, root, null, () => {
|
|
457
|
+
const publicInstance = Renderer.getPublicRootInstance(root);
|
|
458
|
+
if (callback) {
|
|
459
|
+
callback(publicInstance, container);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
Renderer.flushSyncWork();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Legacy entry point. Without a container it connects to the X server
|
|
467
|
+
* (returns a promise in that case).
|
|
468
|
+
*/
|
|
469
|
+
export function render(element, callback, container) {
|
|
470
|
+
if (!container) {
|
|
471
|
+
return connectApp().then((app) =>
|
|
472
|
+
renderIntoContainer(element, app, callback),
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return renderIntoContainer(element, container, callback);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Modern entry point:
|
|
480
|
+
*
|
|
481
|
+
* const root = await createRoot(); // connects via DISPLAY
|
|
482
|
+
* root.render(<App />);
|
|
483
|
+
*
|
|
484
|
+
* Pass an ntk App (or a mock) to render into an existing connection.
|
|
485
|
+
*/
|
|
486
|
+
export async function createRoot(container) {
|
|
487
|
+
const app = container ?? (await connectApp());
|
|
488
|
+
return {
|
|
489
|
+
app,
|
|
490
|
+
render(element, callback) {
|
|
491
|
+
renderIntoContainer(element, app, callback);
|
|
492
|
+
},
|
|
493
|
+
unmount() {
|
|
494
|
+
unmountComponentAtNode(app);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function unmountComponentAtNode(container) {
|
|
500
|
+
const root = roots.get(container);
|
|
501
|
+
if (root) {
|
|
502
|
+
Renderer.updateContainerSync(null, root, null, () => {
|
|
503
|
+
roots.delete(container);
|
|
504
|
+
});
|
|
505
|
+
Renderer.flushSyncWork();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Widget components built purely on the host primitives — no reconciler
|
|
2
|
+
// support needed. Plain createElement (no JSX) so the library stays
|
|
3
|
+
// build-step-free for consumers.
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { labelContent, useControl, useTheme } from './theme.js';
|
|
7
|
+
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* <Button onPress primary disabled …boxProps>label</Button> — the standard
|
|
12
|
+
* push button the examples kept re-implementing: hover/focus feedback,
|
|
13
|
+
* Space/Enter activation, pointer cursor.
|
|
14
|
+
*/
|
|
15
|
+
export function Button({
|
|
16
|
+
children,
|
|
17
|
+
label,
|
|
18
|
+
onPress,
|
|
19
|
+
primary = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
...boxProps
|
|
22
|
+
}) {
|
|
23
|
+
const theme = useTheme();
|
|
24
|
+
const { hover, focused, props } = useControl(disabled, onPress);
|
|
25
|
+
const background = disabled
|
|
26
|
+
? theme.surfaceHover
|
|
27
|
+
: primary
|
|
28
|
+
? hover
|
|
29
|
+
? theme.accentHover
|
|
30
|
+
: theme.accent
|
|
31
|
+
: hover
|
|
32
|
+
? theme.surfaceHover
|
|
33
|
+
: theme.background;
|
|
34
|
+
const color = disabled ? theme.dim : primary ? theme.accentText : theme.text;
|
|
35
|
+
return h(
|
|
36
|
+
'box',
|
|
37
|
+
{
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
gap: 8,
|
|
42
|
+
paddingTop: 8,
|
|
43
|
+
paddingBottom: 8,
|
|
44
|
+
paddingLeft: 16,
|
|
45
|
+
paddingRight: 16,
|
|
46
|
+
borderRadius: 4,
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: disabled
|
|
49
|
+
? theme.border
|
|
50
|
+
: focused
|
|
51
|
+
? primary
|
|
52
|
+
? theme.accentHover
|
|
53
|
+
: theme.borderActive
|
|
54
|
+
: primary
|
|
55
|
+
? theme.accent
|
|
56
|
+
: theme.border,
|
|
57
|
+
backgroundColor: background,
|
|
58
|
+
...props,
|
|
59
|
+
...boxProps,
|
|
60
|
+
},
|
|
61
|
+
labelContent(children ?? label, { color }),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createElement as h } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<Canvas3D>` — the react-three-fiber-shaped entry point to the 3D scene.
|
|
5
|
+
* A thin wrapper over the `<glarea>` host element: it is the surface that
|
|
6
|
+
* owns the GL context, and the scene lives in its children.
|
|
7
|
+
*
|
|
8
|
+
* ```jsx
|
|
9
|
+
* <Canvas3D flexGrow={1} camera={{ position: [3, 3, 6], fov: 50 }}>
|
|
10
|
+
* <mesh rotation={[0.4, 0.8, 0]}>
|
|
11
|
+
* <boxGeometry args={[1, 1, 1]} />
|
|
12
|
+
* <meshBasicMaterial color="#2980b9" />
|
|
13
|
+
* </mesh>
|
|
14
|
+
* </Canvas3D>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Props are `<glarea>`'s (layout props, `clearColor`, `frameLoop`, `glx`,
|
|
18
|
+
* `onCreated`, `onDraw`, `onError`) plus `camera`:
|
|
19
|
+
* `{ position, target, up, fov, near, far, orthographic, zoom }`.
|
|
20
|
+
*
|
|
21
|
+
* The name is `Canvas3D`, not r3f's `Canvas`, because react-x11 already has
|
|
22
|
+
* a `<canvas>` host element — the 2D `onDraw` escape hatch.
|
|
23
|
+
*/
|
|
24
|
+
export function Canvas3D({ children, ...props }) {
|
|
25
|
+
return h('glarea', props, children);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default Canvas3D;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Widget components built purely on the host primitives — no reconciler
|
|
2
|
+
// support needed. Plain createElement (no JSX) so the library stays
|
|
3
|
+
// build-step-free for consumers.
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { labelContent, useControl, useTheme } from './theme.js';
|
|
7
|
+
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* <Checkbox checked onChange disabled>label</Checkbox> — 16px check well +
|
|
12
|
+
* label row; click or Space toggles (onChange receives the next value).
|
|
13
|
+
*/
|
|
14
|
+
export function Checkbox({
|
|
15
|
+
children,
|
|
16
|
+
label,
|
|
17
|
+
checked = false,
|
|
18
|
+
onChange,
|
|
19
|
+
disabled = false,
|
|
20
|
+
...boxProps
|
|
21
|
+
}) {
|
|
22
|
+
const theme = useTheme();
|
|
23
|
+
const { focused, props } = useControl(disabled, () => onChange?.(!checked));
|
|
24
|
+
const fill = disabled ? theme.dim : theme.accent;
|
|
25
|
+
return h(
|
|
26
|
+
'box',
|
|
27
|
+
{
|
|
28
|
+
flexDirection: 'row',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
gap: 8,
|
|
31
|
+
...props,
|
|
32
|
+
...boxProps,
|
|
33
|
+
},
|
|
34
|
+
h(
|
|
35
|
+
'box',
|
|
36
|
+
{
|
|
37
|
+
width: 16,
|
|
38
|
+
height: 16,
|
|
39
|
+
borderRadius: 3,
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
borderColor: checked
|
|
42
|
+
? fill
|
|
43
|
+
: focused
|
|
44
|
+
? theme.borderActive
|
|
45
|
+
: theme.border,
|
|
46
|
+
backgroundColor: checked ? fill : theme.background,
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
},
|
|
50
|
+
checked &&
|
|
51
|
+
h('canvas', {
|
|
52
|
+
width: 10,
|
|
53
|
+
height: 8,
|
|
54
|
+
onDraw: (ctx) => {
|
|
55
|
+
ctx.strokeStyle = theme.accentText;
|
|
56
|
+
ctx.lineWidth = 2;
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.moveTo(1, 4);
|
|
59
|
+
ctx.lineTo(3.5, 6.5);
|
|
60
|
+
ctx.lineTo(9, 1);
|
|
61
|
+
ctx.stroke();
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
labelContent(children ?? label, {
|
|
66
|
+
color: disabled ? theme.dim : theme.text,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|