@blender-ui/react 0.2.0 → 0.3.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 +173 -171
- package/hooks.js +397 -397
- package/index.d.ts +110 -110
- package/index.js +285 -370
- package/jsx-dev-runtime.d.ts +2 -2
- package/jsx-dev-runtime.js +9 -9
- package/jsx-runtime.d.ts +26 -26
- package/jsx-runtime.js +11 -11
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,370 +1,285 @@
|
|
|
1
|
-
// @blender-ui/react: a React renderer for BUI (react-reconciler 0.34 / React 19.3).
|
|
2
|
-
//
|
|
3
|
-
// const { render } = require('@blender-ui/react')
|
|
4
|
-
// render(<div className="p-4 bg-[#4772b3]" onClick={...}>Hello</div>,
|
|
5
|
-
// { title: 'App', width: 400, height: 300 })
|
|
6
|
-
//
|
|
7
|
-
// Host components are HTML elements: React mutates @blender-ui/native's
|
|
8
|
-
// retained tree, which sends only the changes of each commit to the native
|
|
9
|
-
// UI thread. Events arrive as BUI events (onClick, onChange, onKeyDown ...).
|
|
10
|
-
//
|
|
11
|
-
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
12
|
-
'use strict'
|
|
13
|
-
|
|
14
|
-
const React = require('react')
|
|
15
|
-
const Reconciler = require('react-reconciler')
|
|
16
|
-
const { ConcurrentRoot, DefaultEventPriority, DiscreteEventPriority, NoEventPriority } =
|
|
17
|
-
require('react-reconciler/constants')
|
|
18
|
-
const native = require('@blender-ui/native')
|
|
19
|
-
const {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
native.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
updatePriority = prev
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function report (kind) {
|
|
291
|
-
return (err) => console.error(`@blender-ui/react: ${kind}`, err)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// The root component gets render tracking even when created with React's
|
|
295
|
-
// own createElement (components below need this package's JSX runtime or
|
|
296
|
-
// createElement).
|
|
297
|
-
function trackRoot (element) {
|
|
298
|
-
if (!React.isValidElement(element)) return element
|
|
299
|
-
const type = hooks.track(element.type)
|
|
300
|
-
if (type === element.type) return element
|
|
301
|
-
return React.createElement(type, element.key != null ? { ...element.props, key: element.key } : element.props)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** A React root rendering into `window`'s body. */
|
|
305
|
-
function createRoot (window) {
|
|
306
|
-
const container = { window, created: [] }
|
|
307
|
-
const root = reconciler.createContainer(container, ConcurrentRoot, null, false, null, 'bui',
|
|
308
|
-
report('uncaught error'), report('caught error'), report('recoverable error'), null)
|
|
309
|
-
window.on('close', () => reconciler.updateContainer(null, root, null, null))
|
|
310
|
-
return {
|
|
311
|
-
render (element) { reconciler.updateContainer(trackRoot(element), root, null, null) },
|
|
312
|
-
unmount () { reconciler.updateContainer(null, root, null, null) },
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Open a window and render `element` into it.
|
|
318
|
-
* @param {React.ReactNode} element
|
|
319
|
-
* @param {{ title?: string, width?: number, height?: number, resizable?: boolean,
|
|
320
|
-
* gpu?: boolean, slug?: boolean, headless?: boolean, app?: App }} [options]
|
|
321
|
-
* @returns {{ app: App, window: import('@blender-ui/native').Window, root: ReturnType<typeof createRoot> }}
|
|
322
|
-
*/
|
|
323
|
-
function render (element, options = {}) {
|
|
324
|
-
const app = options.app ?? new App({ gpu: options.gpu, slug: options.slug, headless: options.headless, theme: options.theme })
|
|
325
|
-
const window = app.window(options)
|
|
326
|
-
const root = createRoot(window)
|
|
327
|
-
root.render(element)
|
|
328
|
-
return { app, window, root }
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Run `fn` and flush React's work synchronously (tests). */
|
|
332
|
-
function act (fn) {
|
|
333
|
-
const flushSync = reconciler.flushSyncFromReconciler ?? reconciler.flushSync
|
|
334
|
-
return typeof flushSync === 'function' ? flushSync(fn) : fn()
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// React's hooks by React's names: those that hold or react to state are the
|
|
338
|
-
// signal versions (hooks.js), the others are React's own.
|
|
339
|
-
const reactHooks = {}
|
|
340
|
-
for (const k of Object.keys(React)) if (/^use([A-Z]|$)/.test(k)) reactHooks[k] = React[k]
|
|
341
|
-
|
|
342
|
-
// Blender's widgets as components: `<NumberSlider min={0} max={10} />`,
|
|
343
|
-
// `<Button onClick={..}>Apply</Button>`. Always Blender widgets; their
|
|
344
|
-
// lowercase HTML counterparts look like a browser's (Blender's inside `styled`).
|
|
345
|
-
const blenderComponents = {}
|
|
346
|
-
for (const name of Object.keys(native.BLENDER_COMPONENTS)) {
|
|
347
|
-
const C = (props) => React.createElement(name, props)
|
|
348
|
-
Object.defineProperty(C, 'name', { value: name })
|
|
349
|
-
blenderComponents[name] = C
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
module.exports = {
|
|
353
|
-
...blenderComponents,
|
|
354
|
-
...reactHooks,
|
|
355
|
-
...hooks,
|
|
356
|
-
render,
|
|
357
|
-
createRoot,
|
|
358
|
-
act,
|
|
359
|
-
reconciler,
|
|
360
|
-
signal: native.signal,
|
|
361
|
-
computed: native.memo, // `memo` stays React's (components)
|
|
362
|
-
effect: native.effect,
|
|
363
|
-
batch: native.batch,
|
|
364
|
-
untrack: native.untrack,
|
|
365
|
-
createSignal: native.createSignal,
|
|
366
|
-
ref: native.ref,
|
|
367
|
-
isSignal,
|
|
368
|
-
Signal,
|
|
369
|
-
Memo,
|
|
370
|
-
}
|
|
1
|
+
// @blender-ui/react: a React renderer for BUI (react-reconciler 0.34 / React 19.3).
|
|
2
|
+
//
|
|
3
|
+
// const { render } = require('@blender-ui/react')
|
|
4
|
+
// render(<div className="p-4 bg-[#4772b3]" onClick={...}>Hello</div>,
|
|
5
|
+
// { title: 'App', width: 400, height: 300 })
|
|
6
|
+
//
|
|
7
|
+
// Host components are HTML elements: React mutates @blender-ui/native's
|
|
8
|
+
// retained tree, which sends only the changes of each commit to the native
|
|
9
|
+
// UI thread. Events arrive as BUI events (onClick, onChange, onKeyDown ...).
|
|
10
|
+
//
|
|
11
|
+
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
12
|
+
'use strict'
|
|
13
|
+
|
|
14
|
+
const React = require('react')
|
|
15
|
+
const Reconciler = require('react-reconciler')
|
|
16
|
+
const { ConcurrentRoot, DefaultEventPriority, DiscreteEventPriority, NoEventPriority } =
|
|
17
|
+
require('react-reconciler/constants')
|
|
18
|
+
const native = require('@blender-ui/native')
|
|
19
|
+
const { TextNode, Signal, Memo, isSignal } = native
|
|
20
|
+
const { open, blenderWidgets, applyProps, isText, NO_PROPS } = require('@blender-ui/native/host')
|
|
21
|
+
const hooks = require('./hooks')
|
|
22
|
+
|
|
23
|
+
// A signal rendered as a child (`<span>{count}</span>`) becomes this host
|
|
24
|
+
// type: a text node bound to the signal, updated without re-rendering.
|
|
25
|
+
const SIGNAL_TEXT = '#bui-signal-text'
|
|
26
|
+
for (const C of [Signal, Memo]) {
|
|
27
|
+
Object.defineProperties(C.prototype, {
|
|
28
|
+
$$typeof: { value: Symbol.for('react.transitional.element'), configurable: true },
|
|
29
|
+
type: { value: SIGNAL_TEXT, configurable: true },
|
|
30
|
+
key: { value: null, configurable: true },
|
|
31
|
+
ref: { value: null, configurable: true },
|
|
32
|
+
_owner: { value: null, configurable: true },
|
|
33
|
+
// React (dev) marks list children validated here: a fresh object each time
|
|
34
|
+
_store: { get () { return { validated: 1 } }, configurable: true },
|
|
35
|
+
props: { get () { return (this._reactProps ??= { signal: this }) }, configurable: true },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Nodes a render created. React may throw a render away (an interrupted or
|
|
40
|
+
// suspended concurrent render); its nodes are then never attached, and are
|
|
41
|
+
// freed after the next commit.
|
|
42
|
+
function created (container, node) {
|
|
43
|
+
container.created.push(node)
|
|
44
|
+
return node
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sweep (container) {
|
|
48
|
+
const body = container.window.body
|
|
49
|
+
for (const node of container.created) {
|
|
50
|
+
let top = node
|
|
51
|
+
while (top.parentNode !== null) top = top.parentNode
|
|
52
|
+
if (top !== body && container.window._nodes.has(top.id)) top.destroy()
|
|
53
|
+
}
|
|
54
|
+
container.created.length = 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let updatePriority = NoEventPriority
|
|
58
|
+
const ROOT_CONTEXT = {}
|
|
59
|
+
|
|
60
|
+
const host = {
|
|
61
|
+
rendererPackageName: '@blender-ui/react',
|
|
62
|
+
rendererVersion: require('./package.json').version,
|
|
63
|
+
supportsMutation: true,
|
|
64
|
+
supportsPersistence: false,
|
|
65
|
+
supportsHydration: false,
|
|
66
|
+
supportsMicrotasks: true,
|
|
67
|
+
isPrimaryRenderer: true,
|
|
68
|
+
warnsIfNotActing: false,
|
|
69
|
+
noTimeout: -1,
|
|
70
|
+
scheduleTimeout: setTimeout,
|
|
71
|
+
cancelTimeout: clearTimeout,
|
|
72
|
+
scheduleMicrotask: queueMicrotask,
|
|
73
|
+
|
|
74
|
+
getRootHostContext: () => ROOT_CONTEXT, // must not be null (React checks)
|
|
75
|
+
getChildHostContext: (parent) => parent,
|
|
76
|
+
getPublicInstance: (instance) => instance,
|
|
77
|
+
prepareForCommit: () => null,
|
|
78
|
+
resetAfterCommit (container) {
|
|
79
|
+
if (container.created.length > 0) sweep(container)
|
|
80
|
+
container.window.flush() // one native call per commit
|
|
81
|
+
},
|
|
82
|
+
preparePortalMount () {},
|
|
83
|
+
|
|
84
|
+
createInstance (type, props, container) {
|
|
85
|
+
if (type === SIGNAL_TEXT) return created(container, container.window.createTextNode(props.signal))
|
|
86
|
+
const el = container.window.createElement(type)
|
|
87
|
+
applyProps(el, NO_PROPS, props)
|
|
88
|
+
return created(container, el)
|
|
89
|
+
},
|
|
90
|
+
createTextInstance: (text, container) => created(container, container.window.createTextNode(text)),
|
|
91
|
+
appendInitialChild: (parent, child) => parent.appendChild(child),
|
|
92
|
+
finalizeInitialChildren: () => false,
|
|
93
|
+
shouldSetTextContent: (type, props) => isText(props.children),
|
|
94
|
+
|
|
95
|
+
appendChild: (parent, child) => parent.appendChild(child),
|
|
96
|
+
appendChildToContainer: (container, child) => container.window.body.appendChild(child),
|
|
97
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
98
|
+
insertInContainerBefore: (container, child, before) => container.window.body.insertBefore(child, before),
|
|
99
|
+
removeChild (parent, child) {
|
|
100
|
+
parent.removeChild(child)
|
|
101
|
+
child.destroy()
|
|
102
|
+
},
|
|
103
|
+
removeChildFromContainer (container, child) {
|
|
104
|
+
container.window.body.removeChild(child)
|
|
105
|
+
child.destroy()
|
|
106
|
+
},
|
|
107
|
+
clearContainer (container) {
|
|
108
|
+
for (const c of [...container.window.body.childNodes]) container.window.body.removeChild(c)
|
|
109
|
+
},
|
|
110
|
+
// the subtree: text set through textContent has no fiber of its own (freeing twice is a no-op)
|
|
111
|
+
detachDeletedInstance: (node) => node.destroy(),
|
|
112
|
+
|
|
113
|
+
commitUpdate (el, type, prev, next) {
|
|
114
|
+
if (type === SIGNAL_TEXT) {
|
|
115
|
+
if (prev.signal !== next.signal) el.data = next.signal
|
|
116
|
+
} else {
|
|
117
|
+
applyProps(el, prev, next)
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
commitTextUpdate: (text, prev, next) => { text.data = next },
|
|
121
|
+
commitMount () {},
|
|
122
|
+
resetTextContent: (el) => { el.textContent = '' },
|
|
123
|
+
hideInstance (el) {
|
|
124
|
+
if (el instanceof TextNode) el.data = '' // a signal's text: unbound while hidden
|
|
125
|
+
else el.setAttribute('hidden', '')
|
|
126
|
+
},
|
|
127
|
+
unhideInstance (el, props) {
|
|
128
|
+
if (el instanceof TextNode) el.data = props.signal
|
|
129
|
+
else if (!props.hidden) el.removeAttribute('hidden')
|
|
130
|
+
},
|
|
131
|
+
hideTextInstance: (text) => { text._hidden = text.data; text.data = '' },
|
|
132
|
+
unhideTextInstance: (text, value) => { text.data = value },
|
|
133
|
+
|
|
134
|
+
setCurrentUpdatePriority: (p) => { updatePriority = p },
|
|
135
|
+
getCurrentUpdatePriority: () => updatePriority,
|
|
136
|
+
resolveUpdatePriority: () => updatePriority || DefaultEventPriority,
|
|
137
|
+
resolveEventType: () => null,
|
|
138
|
+
resolveEventTimeStamp: () => -1.1,
|
|
139
|
+
shouldAttemptEagerTransition: () => false,
|
|
140
|
+
trackSchedulerEvent () {},
|
|
141
|
+
requestPostPaintCallback () {},
|
|
142
|
+
maySuspendCommit: () => false,
|
|
143
|
+
preloadInstance: () => true,
|
|
144
|
+
startSuspendingCommit () {},
|
|
145
|
+
suspendInstance () {},
|
|
146
|
+
waitForCommitToBeReady: () => null,
|
|
147
|
+
NotPendingTransition: null,
|
|
148
|
+
HostTransitionContext: React.createContext(null),
|
|
149
|
+
resetFormInstance () {},
|
|
150
|
+
getInstanceFromNode: () => null,
|
|
151
|
+
getInstanceFromScope: () => null,
|
|
152
|
+
beforeActiveInstanceBlur () {},
|
|
153
|
+
afterActiveInstanceBlur () {},
|
|
154
|
+
prepareScopeUpdate () {},
|
|
155
|
+
bindToConsole: (method, args) => Function.prototype.bind.apply(console[method], [console, ...args]),
|
|
156
|
+
|
|
157
|
+
// Commits that wait on resources: none here.
|
|
158
|
+
maySuspendCommitOnUpdate: () => false,
|
|
159
|
+
maySuspendCommitInSyncRender: () => false,
|
|
160
|
+
getSuspendedCommitReason: () => null,
|
|
161
|
+
|
|
162
|
+
// View transitions (<ViewTransition>): not animated here. As React DOM does
|
|
163
|
+
// without the browser API: run the commit's phases now and report none.
|
|
164
|
+
startViewTransition (suspendedState, container, types, mutation, layout, afterMutation, spawnedWork) {
|
|
165
|
+
mutation()
|
|
166
|
+
layout()
|
|
167
|
+
spawnedWork()
|
|
168
|
+
return null
|
|
169
|
+
},
|
|
170
|
+
startGestureTransition: () => null,
|
|
171
|
+
stopViewTransition () {},
|
|
172
|
+
suspendOnActiveViewTransition () {},
|
|
173
|
+
addViewTransitionFinishedListener (transition, callback) { callback() },
|
|
174
|
+
createViewTransitionInstance: (name) => ({ name, group: null, imagePair: null, old: null, new: null }),
|
|
175
|
+
applyViewTransitionName () {},
|
|
176
|
+
restoreViewTransitionName () {},
|
|
177
|
+
cancelViewTransitionName () {},
|
|
178
|
+
cancelRootViewTransitionName () {},
|
|
179
|
+
restoreRootViewTransitionName () {},
|
|
180
|
+
cloneRootViewTransitionContainer: (container) => container,
|
|
181
|
+
removeRootViewTransitionClone () {},
|
|
182
|
+
measureInstance: () => null,
|
|
183
|
+
measureClonedInstance: () => null,
|
|
184
|
+
wasInstanceInViewport: () => true,
|
|
185
|
+
hasInstanceChanged: () => false,
|
|
186
|
+
hasInstanceAffectedParent: () => false,
|
|
187
|
+
getCurrentGestureOffset: () => 0,
|
|
188
|
+
|
|
189
|
+
// <Fragment ref>: the fragment's fiber, nothing more (no DOM-like API yet).
|
|
190
|
+
createFragmentInstance: (fiber) => ({ _fragmentFiber: fiber }),
|
|
191
|
+
updateFragmentInstanceFiber (fiber, instance) { instance._fragmentFiber = fiber },
|
|
192
|
+
commitNewChildToFragmentInstance () {},
|
|
193
|
+
deleteChildFromFragmentInstance () {},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const reconciler = Reconciler(host)
|
|
197
|
+
|
|
198
|
+
// Events: run handlers as discrete updates (React DOM does the same), so a
|
|
199
|
+
// click's state change commits right away.
|
|
200
|
+
const nativeDispatch = native.Element.prototype._dispatch
|
|
201
|
+
native.Element.prototype._dispatch = function (raw) {
|
|
202
|
+
const prev = updatePriority
|
|
203
|
+
updatePriority = DiscreteEventPriority
|
|
204
|
+
try {
|
|
205
|
+
nativeDispatch.call(this, raw)
|
|
206
|
+
} finally {
|
|
207
|
+
updatePriority = prev
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function report (kind) {
|
|
212
|
+
return (err) => console.error(`@blender-ui/react: ${kind}`, err)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The root component gets render tracking even when created with React's
|
|
216
|
+
// own createElement (components below need this package's JSX runtime or
|
|
217
|
+
// createElement).
|
|
218
|
+
function trackRoot (element) {
|
|
219
|
+
if (!React.isValidElement(element)) return element
|
|
220
|
+
const type = hooks.track(element.type)
|
|
221
|
+
if (type === element.type) return element
|
|
222
|
+
return React.createElement(type, element.key != null ? { ...element.props, key: element.key } : element.props)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A React root rendering into `window`'s body. */
|
|
226
|
+
function createRoot (window) {
|
|
227
|
+
const container = { window, created: [] }
|
|
228
|
+
const root = reconciler.createContainer(container, ConcurrentRoot, null, false, null, 'bui',
|
|
229
|
+
report('uncaught error'), report('caught error'), report('recoverable error'), null)
|
|
230
|
+
window.on('close', () => reconciler.updateContainer(null, root, null, null))
|
|
231
|
+
return {
|
|
232
|
+
render (element) { reconciler.updateContainer(trackRoot(element), root, null, null) },
|
|
233
|
+
unmount () { reconciler.updateContainer(null, root, null, null) },
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Open a window and render `element` into it.
|
|
239
|
+
* @param {React.ReactNode} element
|
|
240
|
+
* @param {{ title?: string, width?: number, height?: number, resizable?: boolean,
|
|
241
|
+
* gpu?: boolean, slug?: boolean, headless?: boolean, app?: import('@blender-ui/native').App }} [options]
|
|
242
|
+
* @returns {{ app: import('@blender-ui/native').App, window: import('@blender-ui/native').Window, root: ReturnType<typeof createRoot> }}
|
|
243
|
+
*/
|
|
244
|
+
function render (element, options = {}) {
|
|
245
|
+
const { app, window } = open(options)
|
|
246
|
+
const root = createRoot(window)
|
|
247
|
+
root.render(element)
|
|
248
|
+
return { app, window, root }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Run `fn` and flush React's work synchronously (tests). */
|
|
252
|
+
function act (fn) {
|
|
253
|
+
const flushSync = reconciler.flushSyncFromReconciler ?? reconciler.flushSync
|
|
254
|
+
return typeof flushSync === 'function' ? flushSync(fn) : fn()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// React's hooks by React's names: those that hold or react to state are the
|
|
258
|
+
// signal versions (hooks.js), the others are React's own.
|
|
259
|
+
const reactHooks = {}
|
|
260
|
+
for (const k of Object.keys(React)) if (/^use([A-Z]|$)/.test(k)) reactHooks[k] = React[k]
|
|
261
|
+
|
|
262
|
+
// Blender's widgets as components: `<NumberSlider min={0} max={10} />`,
|
|
263
|
+
// `<Button onClick={..}>Apply</Button>`. Always Blender widgets; their
|
|
264
|
+
// lowercase HTML counterparts look like a browser's (Blender's inside `styled`).
|
|
265
|
+
const blenderComponents = blenderWidgets((name) => (props) => React.createElement(name, props))
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
...blenderComponents,
|
|
269
|
+
...reactHooks,
|
|
270
|
+
...hooks,
|
|
271
|
+
render,
|
|
272
|
+
createRoot,
|
|
273
|
+
act,
|
|
274
|
+
reconciler,
|
|
275
|
+
signal: native.signal,
|
|
276
|
+
computed: native.memo, // `memo` stays React's (components)
|
|
277
|
+
effect: native.effect,
|
|
278
|
+
batch: native.batch,
|
|
279
|
+
untrack: native.untrack,
|
|
280
|
+
createSignal: native.createSignal,
|
|
281
|
+
ref: native.ref,
|
|
282
|
+
isSignal,
|
|
283
|
+
Signal,
|
|
284
|
+
Memo,
|
|
285
|
+
}
|