onejs-react 0.1.0 → 0.1.1
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 +84 -0
- package/package.json +2 -1
- package/src/__tests__/components.test.tsx +36 -0
- package/src/__tests__/host-config.test.ts +141 -99
- package/src/__tests__/mocks.ts +17 -1
- package/src/__tests__/setup.ts +23 -11
- package/src/components.tsx +77 -48
- package/src/error-boundary.tsx +175 -0
- package/src/host-config.ts +341 -158
- package/src/index.ts +35 -2
- package/src/renderer.ts +50 -23
- package/src/screen.tsx +1 -1
- package/src/style-parser.ts +42 -70
- package/src/types.ts +196 -5
package/src/host-config.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type {HostConfig} from 'react-reconciler';
|
|
2
|
-
import type {BaseProps, ViewStyle} from './types';
|
|
2
|
+
import type {BaseProps, ViewStyle, VisualElement} from './types';
|
|
3
3
|
import {parseStyleValue} from './style-parser';
|
|
4
4
|
|
|
5
|
+
// CSObject is an alias for VisualElement - they represent the same C# objects
|
|
6
|
+
type CSObject = VisualElement;
|
|
7
|
+
|
|
5
8
|
// Global declarations for QuickJS environment
|
|
6
9
|
declare function setTimeout(callback: () => void, ms?: number): number;
|
|
7
10
|
|
|
@@ -32,6 +35,7 @@ declare const CS: {
|
|
|
32
35
|
UnityEngine: {
|
|
33
36
|
UIElements: {
|
|
34
37
|
VisualElement: new () => CSObject;
|
|
38
|
+
TextElement: new () => CSObject;
|
|
35
39
|
Label: new () => CSObject;
|
|
36
40
|
Button: new () => CSObject;
|
|
37
41
|
TextField: new () => CSObject;
|
|
@@ -51,6 +55,14 @@ declare const CS: {
|
|
|
51
55
|
CollectionVirtualizationMethod: CSEnum;
|
|
52
56
|
};
|
|
53
57
|
};
|
|
58
|
+
OneJS: {
|
|
59
|
+
GPU: {
|
|
60
|
+
GPUBridge: {
|
|
61
|
+
SetElementBackgroundImage: (element: CSObject, rtHandle: number) => void;
|
|
62
|
+
ClearElementBackgroundImage: (element: CSObject) => void;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
54
66
|
};
|
|
55
67
|
|
|
56
68
|
declare const __eventAPI: {
|
|
@@ -59,24 +71,6 @@ declare const __eventAPI: {
|
|
|
59
71
|
removeAllEventListeners: (element: CSObject) => void;
|
|
60
72
|
};
|
|
61
73
|
|
|
62
|
-
interface CSObject {
|
|
63
|
-
__csHandle: number;
|
|
64
|
-
__csType: string;
|
|
65
|
-
Add: (child: CSObject) => void;
|
|
66
|
-
Insert: (index: number, child: CSObject) => void;
|
|
67
|
-
Remove: (child: CSObject) => void;
|
|
68
|
-
RemoveAt: (index: number) => void;
|
|
69
|
-
IndexOf: (child: CSObject) => number;
|
|
70
|
-
Clear: () => void;
|
|
71
|
-
style: CSStyle;
|
|
72
|
-
text?: string;
|
|
73
|
-
value?: unknown;
|
|
74
|
-
label?: string;
|
|
75
|
-
AddToClassList: (className: string) => void;
|
|
76
|
-
RemoveFromClassList: (className: string) => void;
|
|
77
|
-
ClearClassList: () => void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
74
|
interface CSStyle {
|
|
81
75
|
[key: string]: unknown;
|
|
82
76
|
}
|
|
@@ -132,6 +126,10 @@ interface CSListView extends CSObject {
|
|
|
132
126
|
Rebuild: () => void;
|
|
133
127
|
}
|
|
134
128
|
|
|
129
|
+
// Elements that merge text children into their text property instead of adding as visual children
|
|
130
|
+
// This enables <Label>Hello {"World"}</Label> to render as single line, matching React Native behavior
|
|
131
|
+
const TEXT_MERGE_TYPES = new Set(['ojs-label', 'ojs-text', 'ojs-button']);
|
|
132
|
+
|
|
135
133
|
// Instance type used by the reconciler
|
|
136
134
|
export interface Instance {
|
|
137
135
|
element: CSObject;
|
|
@@ -139,6 +137,12 @@ export interface Instance {
|
|
|
139
137
|
props: BaseProps;
|
|
140
138
|
eventHandlers: Map<string, Function>;
|
|
141
139
|
appliedStyleKeys: Set<string>; // Track which style properties are currently applied
|
|
140
|
+
// For text-merging parents: ordered list of merged text children
|
|
141
|
+
mergedTextChildren?: Instance[];
|
|
142
|
+
// For merged text children: reference to parent they're merged into
|
|
143
|
+
mergedInto?: Instance;
|
|
144
|
+
// Set to true when a non-text child is added, disabling further text merging
|
|
145
|
+
hasMixedContent?: boolean;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
export type TextInstance = Instance; // For Label elements with text content
|
|
@@ -149,6 +153,7 @@ export type ChildSet = never; // Not using persistent mode
|
|
|
149
153
|
// Element types use 'ojs-' prefix to avoid conflicts with HTML/SVG in @types/react
|
|
150
154
|
const TYPE_MAP: Record<string, () => CSObject> = {
|
|
151
155
|
'ojs-view': () => new CS.UnityEngine.UIElements.VisualElement(),
|
|
156
|
+
'ojs-text': () => new CS.UnityEngine.UIElements.TextElement(),
|
|
152
157
|
'ojs-label': () => new CS.UnityEngine.UIElements.Label(),
|
|
153
158
|
'ojs-button': () => new CS.UnityEngine.UIElements.Button(),
|
|
154
159
|
'ojs-textfield': () => new CS.UnityEngine.UIElements.TextField(),
|
|
@@ -161,17 +166,68 @@ const TYPE_MAP: Record<string, () => CSObject> = {
|
|
|
161
166
|
|
|
162
167
|
// Event prop to event type mapping
|
|
163
168
|
const EVENT_PROPS: Record<string, string> = {
|
|
169
|
+
// Click
|
|
164
170
|
onClick: 'click',
|
|
171
|
+
|
|
172
|
+
// Pointer events
|
|
165
173
|
onPointerDown: 'pointerdown',
|
|
166
174
|
onPointerUp: 'pointerup',
|
|
167
175
|
onPointerMove: 'pointermove',
|
|
168
176
|
onPointerEnter: 'pointerenter',
|
|
169
177
|
onPointerLeave: 'pointerleave',
|
|
178
|
+
onPointerCancel: 'pointercancel',
|
|
179
|
+
onPointerCapture: 'pointercapture',
|
|
180
|
+
onPointerCaptureOut: 'pointercaptureout',
|
|
181
|
+
onPointerStationary: 'pointerstationary',
|
|
182
|
+
|
|
183
|
+
// Mouse events
|
|
184
|
+
onMouseDown: 'mousedown',
|
|
185
|
+
onMouseUp: 'mouseup',
|
|
186
|
+
onMouseMove: 'mousemove',
|
|
187
|
+
onMouseEnter: 'mouseenter',
|
|
188
|
+
onMouseLeave: 'mouseleave',
|
|
189
|
+
onMouseOver: 'mouseover',
|
|
190
|
+
onMouseOut: 'mouseout',
|
|
191
|
+
onWheel: 'wheel',
|
|
192
|
+
onContextClick: 'contextclick',
|
|
193
|
+
|
|
194
|
+
// Focus events
|
|
170
195
|
onFocus: 'focus',
|
|
171
196
|
onBlur: 'blur',
|
|
197
|
+
onFocusIn: 'focusin',
|
|
198
|
+
onFocusOut: 'focusout',
|
|
199
|
+
|
|
200
|
+
// Keyboard events
|
|
172
201
|
onKeyDown: 'keydown',
|
|
173
202
|
onKeyUp: 'keyup',
|
|
203
|
+
|
|
204
|
+
// Input events
|
|
174
205
|
onChange: 'change',
|
|
206
|
+
onInput: 'input',
|
|
207
|
+
|
|
208
|
+
// Drag events
|
|
209
|
+
onDragEnter: 'dragenter',
|
|
210
|
+
onDragLeave: 'dragleave',
|
|
211
|
+
onDragUpdated: 'dragupdated',
|
|
212
|
+
onDragPerform: 'dragperform',
|
|
213
|
+
onDragExited: 'dragexited',
|
|
214
|
+
|
|
215
|
+
// Geometry events
|
|
216
|
+
onGeometryChanged: 'geometrychanged',
|
|
217
|
+
|
|
218
|
+
// Navigation events
|
|
219
|
+
onNavigationMove: 'navigationmove',
|
|
220
|
+
onNavigationSubmit: 'navigationsubmit',
|
|
221
|
+
onNavigationCancel: 'navigationcancel',
|
|
222
|
+
|
|
223
|
+
// Tooltip
|
|
224
|
+
onTooltip: 'tooltip',
|
|
225
|
+
|
|
226
|
+
// Transition events
|
|
227
|
+
onTransitionRun: 'transitionrun',
|
|
228
|
+
onTransitionStart: 'transitionstart',
|
|
229
|
+
onTransitionEnd: 'transitionend',
|
|
230
|
+
onTransitionCancel: 'transitioncancel',
|
|
175
231
|
};
|
|
176
232
|
|
|
177
233
|
// Shorthand style properties that expand to multiple properties
|
|
@@ -237,6 +293,35 @@ function getExpandedStyleKeys(style: ViewStyle | undefined): Set<string> {
|
|
|
237
293
|
return keys;
|
|
238
294
|
}
|
|
239
295
|
|
|
296
|
+
/**
|
|
297
|
+
* RenderTexture-like object for backgroundImage style property.
|
|
298
|
+
* Can be either:
|
|
299
|
+
* - A marker object with __rtHandle (from rt.getUnityObject())
|
|
300
|
+
* - A RenderTexture object directly (has __handle property)
|
|
301
|
+
*/
|
|
302
|
+
interface RenderTextureRef {
|
|
303
|
+
__rtHandle?: number;
|
|
304
|
+
__handle?: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if a value is a RenderTexture or RenderTexture handle marker.
|
|
309
|
+
* Supports both:
|
|
310
|
+
* - Direct RenderTexture objects (have __handle)
|
|
311
|
+
* - Marker objects from getUnityObject() (have __rtHandle)
|
|
312
|
+
*/
|
|
313
|
+
function isRenderTextureHandle(value: unknown): value is RenderTextureRef {
|
|
314
|
+
if (typeof value !== "object" || value === null) return false;
|
|
315
|
+
return "__rtHandle" in value || "__handle" in value;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get the RT handle from a RenderTexture-like object.
|
|
320
|
+
*/
|
|
321
|
+
function getRenderTextureHandle(value: RenderTextureRef): number {
|
|
322
|
+
return value.__rtHandle ?? value.__handle ?? -1;
|
|
323
|
+
}
|
|
324
|
+
|
|
240
325
|
// Apply style properties to element, returns the set of applied keys
|
|
241
326
|
function applyStyle(element: CSObject, style: ViewStyle | undefined): Set<string> {
|
|
242
327
|
const appliedKeys = new Set<string>();
|
|
@@ -255,6 +340,14 @@ function applyStyle(element: CSObject, style: ViewStyle | undefined): Set<string
|
|
|
255
340
|
s[prop] = parsed;
|
|
256
341
|
appliedKeys.add(prop);
|
|
257
342
|
}
|
|
343
|
+
} else if (key === "backgroundImage" && isRenderTextureHandle(value)) {
|
|
344
|
+
// Special handling for RenderTexture background images
|
|
345
|
+
// Supports both direct RenderTexture objects and marker objects
|
|
346
|
+
const handle = getRenderTextureHandle(value);
|
|
347
|
+
if (handle >= 0) {
|
|
348
|
+
CS.OneJS.GPU.GPUBridge.SetElementBackgroundImage(element, handle);
|
|
349
|
+
}
|
|
350
|
+
appliedKeys.add(key);
|
|
258
351
|
} else {
|
|
259
352
|
// Parse and apply individual property
|
|
260
353
|
s[key] = parseStyleValue(key, value);
|
|
@@ -269,8 +362,13 @@ function clearRemovedStyles(element: CSObject, oldKeys: Set<string>, newKeys: Se
|
|
|
269
362
|
const s = element.style;
|
|
270
363
|
for (const key of oldKeys) {
|
|
271
364
|
if (!newKeys.has(key)) {
|
|
272
|
-
|
|
273
|
-
|
|
365
|
+
if (key === "backgroundImage") {
|
|
366
|
+
// Special handling for backgroundImage - use GPUBridge to clear
|
|
367
|
+
CS.OneJS.GPU.GPUBridge.ClearElementBackgroundImage(element);
|
|
368
|
+
} else {
|
|
369
|
+
// Setting to undefined clears the inline style, falling back to USS
|
|
370
|
+
s[key] = undefined;
|
|
371
|
+
}
|
|
274
372
|
}
|
|
275
373
|
}
|
|
276
374
|
}
|
|
@@ -334,125 +432,173 @@ function applyEvents(instance: Instance, props: BaseProps) {
|
|
|
334
432
|
}
|
|
335
433
|
}
|
|
336
434
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
435
|
+
// MARK: Text Merging
|
|
436
|
+
// Rebuild concatenated text from merged text children
|
|
437
|
+
function rebuildMergedText(instance: Instance) {
|
|
438
|
+
const children = instance.mergedTextChildren;
|
|
439
|
+
if (!children || children.length === 0) {
|
|
440
|
+
// No merged children - clear text (or keep prop-based text?)
|
|
441
|
+
// For now, set to empty - if user wants text, they should use children
|
|
442
|
+
instance.element.text = '';
|
|
443
|
+
return;
|
|
342
444
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
445
|
+
instance.element.text = children.map(c => c.element.text || '').join('');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Check if a child should be merged into parent's text property
|
|
449
|
+
function shouldMergeText(parentInstance: Instance, child: Instance): boolean {
|
|
450
|
+
// Don't merge if parent has mixed content (non-text children were added)
|
|
451
|
+
if (parentInstance.hasMixedContent) return false;
|
|
452
|
+
return TEXT_MERGE_TYPES.has(parentInstance.type) && child.type === 'text';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// "Unmerge" all text children - add them as actual visual children
|
|
456
|
+
// Called when a non-text child is added, breaking the pure-text assumption
|
|
457
|
+
function unmergTextChildren(parentInstance: Instance) {
|
|
458
|
+
const children = parentInstance.mergedTextChildren;
|
|
459
|
+
if (!children || children.length === 0) return;
|
|
460
|
+
|
|
461
|
+
// Clear parent's merged text
|
|
462
|
+
parentInstance.element.text = '';
|
|
463
|
+
|
|
464
|
+
// Add each merged text child as an actual visual child
|
|
465
|
+
for (const child of children) {
|
|
466
|
+
child.mergedInto = undefined;
|
|
467
|
+
parentInstance.element.Add(child.element);
|
|
346
468
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
469
|
+
|
|
470
|
+
// Clear the merged children list
|
|
471
|
+
parentInstance.mergedTextChildren = undefined;
|
|
472
|
+
parentInstance.hasMixedContent = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Handle adding a non-text child to a text-merge parent
|
|
476
|
+
// This triggers unmerging of any previously merged text
|
|
477
|
+
function handleNonTextChild(parentInstance: Instance) {
|
|
478
|
+
if (TEXT_MERGE_TYPES.has(parentInstance.type) && !parentInstance.hasMixedContent) {
|
|
479
|
+
unmergTextChildren(parentInstance);
|
|
350
480
|
}
|
|
481
|
+
}
|
|
351
482
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
sv.mode = CS.UnityEngine.UIElements.ScrollViewMode[props.mode as string];
|
|
357
|
-
}
|
|
358
|
-
if (props.horizontalScrollerVisibility !== undefined) {
|
|
359
|
-
sv.horizontalScrollerVisibility = CS.UnityEngine.UIElements.ScrollerVisibility[props.horizontalScrollerVisibility as string];
|
|
360
|
-
}
|
|
361
|
-
if (props.verticalScrollerVisibility !== undefined) {
|
|
362
|
-
sv.verticalScrollerVisibility = CS.UnityEngine.UIElements.ScrollerVisibility[props.verticalScrollerVisibility as string];
|
|
363
|
-
}
|
|
364
|
-
if (props.elasticity !== undefined) {
|
|
365
|
-
sv.elasticity = props.elasticity as number;
|
|
366
|
-
}
|
|
367
|
-
if (props.elasticAnimationIntervalMs !== undefined) {
|
|
368
|
-
sv.elasticAnimationIntervalMs = props.elasticAnimationIntervalMs as number;
|
|
369
|
-
}
|
|
370
|
-
if (props.scrollDecelerationRate !== undefined) {
|
|
371
|
-
sv.scrollDecelerationRate = props.scrollDecelerationRate as number;
|
|
372
|
-
}
|
|
373
|
-
if (props.mouseWheelScrollSize !== undefined) {
|
|
374
|
-
sv.mouseWheelScrollSize = props.mouseWheelScrollSize as number;
|
|
375
|
-
}
|
|
376
|
-
if (props.horizontalPageSize !== undefined) {
|
|
377
|
-
sv.horizontalPageSize = props.horizontalPageSize as number;
|
|
378
|
-
}
|
|
379
|
-
if (props.verticalPageSize !== undefined) {
|
|
380
|
-
sv.verticalPageSize = props.verticalPageSize as number;
|
|
381
|
-
}
|
|
382
|
-
if (props.touchScrollBehavior !== undefined) {
|
|
383
|
-
sv.touchScrollBehavior = CS.UnityEngine.UIElements.TouchScrollBehavior[props.touchScrollBehavior as string];
|
|
384
|
-
}
|
|
385
|
-
if (props.nestedInteractionKind !== undefined) {
|
|
386
|
-
sv.nestedInteractionKind = CS.UnityEngine.UIElements.NestedInteractionKind[props.nestedInteractionKind as string];
|
|
387
|
-
}
|
|
483
|
+
// Append a text child to a text-merging parent
|
|
484
|
+
function appendMergedTextChild(parentInstance: Instance, child: Instance) {
|
|
485
|
+
if (!parentInstance.mergedTextChildren) {
|
|
486
|
+
parentInstance.mergedTextChildren = [];
|
|
388
487
|
}
|
|
488
|
+
parentInstance.mergedTextChildren.push(child);
|
|
489
|
+
child.mergedInto = parentInstance;
|
|
490
|
+
rebuildMergedText(parentInstance);
|
|
491
|
+
}
|
|
389
492
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
493
|
+
// Insert a text child before another in a text-merging parent
|
|
494
|
+
function insertMergedTextChild(parentInstance: Instance, child: Instance, beforeChild: Instance) {
|
|
495
|
+
if (!parentInstance.mergedTextChildren) {
|
|
496
|
+
parentInstance.mergedTextChildren = [];
|
|
497
|
+
}
|
|
498
|
+
const index = parentInstance.mergedTextChildren.indexOf(beforeChild);
|
|
499
|
+
if (index >= 0) {
|
|
500
|
+
parentInstance.mergedTextChildren.splice(index, 0, child);
|
|
501
|
+
} else {
|
|
502
|
+
parentInstance.mergedTextChildren.push(child);
|
|
503
|
+
}
|
|
504
|
+
child.mergedInto = parentInstance;
|
|
505
|
+
rebuildMergedText(parentInstance);
|
|
506
|
+
}
|
|
393
507
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (props.bindItem !== undefined) {
|
|
402
|
-
lv.bindItem = props.bindItem as (element: CSObject, index: number) => void;
|
|
403
|
-
}
|
|
404
|
-
if (props.unbindItem !== undefined) {
|
|
405
|
-
lv.unbindItem = props.unbindItem as (element: CSObject, index: number) => void;
|
|
406
|
-
}
|
|
407
|
-
if (props.destroyItem !== undefined) {
|
|
408
|
-
lv.destroyItem = props.destroyItem as (element: CSObject) => void;
|
|
508
|
+
// Remove a text child from a text-merging parent
|
|
509
|
+
function removeMergedTextChild(parentInstance: Instance, child: Instance) {
|
|
510
|
+
const children = parentInstance.mergedTextChildren;
|
|
511
|
+
if (children) {
|
|
512
|
+
const index = children.indexOf(child);
|
|
513
|
+
if (index >= 0) {
|
|
514
|
+
children.splice(index, 1);
|
|
409
515
|
}
|
|
516
|
+
}
|
|
517
|
+
child.mergedInto = undefined;
|
|
518
|
+
rebuildMergedText(parentInstance);
|
|
519
|
+
}
|
|
410
520
|
|
|
411
|
-
|
|
412
|
-
if (props.fixedItemHeight !== undefined) {
|
|
413
|
-
lv.fixedItemHeight = props.fixedItemHeight as number;
|
|
414
|
-
}
|
|
415
|
-
if (props.virtualizationMethod !== undefined) {
|
|
416
|
-
lv.virtualizationMethod = CS.UnityEngine.UIElements.CollectionVirtualizationMethod[props.virtualizationMethod as string];
|
|
417
|
-
}
|
|
521
|
+
// MARK: Component-specific prop handlers
|
|
418
522
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
if (props.selectedIndices !== undefined) {
|
|
427
|
-
lv.selectedIndices = props.selectedIndices as number[];
|
|
428
|
-
}
|
|
523
|
+
// Apply common props (text, value, label)
|
|
524
|
+
function applyCommonProps(element: CSObject, props: Record<string, unknown>) {
|
|
525
|
+
if (props.text !== undefined) (element as { text: string }).text = props.text as string;
|
|
526
|
+
if (props.value !== undefined) (element as { value: unknown }).value = props.value;
|
|
527
|
+
if (props.label !== undefined) (element as { label: string }).label = props.label as string;
|
|
528
|
+
}
|
|
429
529
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
530
|
+
// Helper to set enum prop if defined
|
|
531
|
+
function setEnumProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string, enumType: CSEnum) {
|
|
532
|
+
if (props[propKey] !== undefined) {
|
|
533
|
+
(target as Record<string, unknown>)[key as string] = enumType[props[propKey] as string];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
437
536
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
if (props.showAddRemoveFooter !== undefined) {
|
|
446
|
-
lv.showAddRemoveFooter = props.showAddRemoveFooter as boolean;
|
|
447
|
-
}
|
|
537
|
+
// Helper to set value prop if defined
|
|
538
|
+
function setValueProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string) {
|
|
539
|
+
if (props[propKey] !== undefined) {
|
|
540
|
+
(target as Record<string, unknown>)[key as string] = props[propKey];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
448
543
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
544
|
+
// Apply ScrollView-specific properties
|
|
545
|
+
function applyScrollViewProps(element: CSScrollView, props: Record<string, unknown>) {
|
|
546
|
+
const UIE = CS.UnityEngine.UIElements;
|
|
547
|
+
setEnumProp(element, 'mode', props, 'mode', UIE.ScrollViewMode);
|
|
548
|
+
setEnumProp(element, 'horizontalScrollerVisibility', props, 'horizontalScrollerVisibility', UIE.ScrollerVisibility);
|
|
549
|
+
setEnumProp(element, 'verticalScrollerVisibility', props, 'verticalScrollerVisibility', UIE.ScrollerVisibility);
|
|
550
|
+
setEnumProp(element, 'touchScrollBehavior', props, 'touchScrollBehavior', UIE.TouchScrollBehavior);
|
|
551
|
+
setEnumProp(element, 'nestedInteractionKind', props, 'nestedInteractionKind', UIE.NestedInteractionKind);
|
|
552
|
+
setValueProp(element, 'elasticity', props, 'elasticity');
|
|
553
|
+
setValueProp(element, 'elasticAnimationIntervalMs', props, 'elasticAnimationIntervalMs');
|
|
554
|
+
setValueProp(element, 'scrollDecelerationRate', props, 'scrollDecelerationRate');
|
|
555
|
+
setValueProp(element, 'mouseWheelScrollSize', props, 'mouseWheelScrollSize');
|
|
556
|
+
setValueProp(element, 'horizontalPageSize', props, 'horizontalPageSize');
|
|
557
|
+
setValueProp(element, 'verticalPageSize', props, 'verticalPageSize');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Apply ListView-specific properties
|
|
561
|
+
function applyListViewProps(element: CSListView, props: Record<string, unknown>) {
|
|
562
|
+
const UIE = CS.UnityEngine.UIElements;
|
|
563
|
+
|
|
564
|
+
// Data binding callbacks
|
|
565
|
+
setValueProp(element, 'itemsSource', props, 'itemsSource');
|
|
566
|
+
setValueProp(element, 'makeItem', props, 'makeItem');
|
|
567
|
+
setValueProp(element, 'bindItem', props, 'bindItem');
|
|
568
|
+
setValueProp(element, 'unbindItem', props, 'unbindItem');
|
|
569
|
+
setValueProp(element, 'destroyItem', props, 'destroyItem');
|
|
570
|
+
|
|
571
|
+
// Virtualization
|
|
572
|
+
setValueProp(element, 'fixedItemHeight', props, 'fixedItemHeight');
|
|
573
|
+
setEnumProp(element, 'virtualizationMethod', props, 'virtualizationMethod', UIE.CollectionVirtualizationMethod);
|
|
574
|
+
|
|
575
|
+
// Selection
|
|
576
|
+
setEnumProp(element, 'selectionType', props, 'selectionType', UIE.SelectionType);
|
|
577
|
+
setValueProp(element, 'selectedIndex', props, 'selectedIndex');
|
|
578
|
+
setValueProp(element, 'selectedIndices', props, 'selectedIndices');
|
|
579
|
+
|
|
580
|
+
// Reordering
|
|
581
|
+
setValueProp(element, 'reorderable', props, 'reorderable');
|
|
582
|
+
setEnumProp(element, 'reorderMode', props, 'reorderMode', UIE.ListViewReorderMode);
|
|
583
|
+
|
|
584
|
+
// Header/Footer
|
|
585
|
+
setValueProp(element, 'showFoldoutHeader', props, 'showFoldoutHeader');
|
|
586
|
+
setValueProp(element, 'headerTitle', props, 'headerTitle');
|
|
587
|
+
setValueProp(element, 'showAddRemoveFooter', props, 'showAddRemoveFooter');
|
|
588
|
+
|
|
589
|
+
// Appearance
|
|
590
|
+
setValueProp(element, 'showBorder', props, 'showBorder');
|
|
591
|
+
setEnumProp(element, 'showAlternatingRowBackgrounds', props, 'showAlternatingRowBackgrounds', UIE.AlternatingRowBackground);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Apply component-specific props based on element type
|
|
595
|
+
function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>) {
|
|
596
|
+
applyCommonProps(element, props);
|
|
597
|
+
|
|
598
|
+
if (type === 'ojs-scrollview') {
|
|
599
|
+
applyScrollViewProps(element as CSScrollView, props);
|
|
600
|
+
} else if (type === 'ojs-listview') {
|
|
601
|
+
applyListViewProps(element as CSListView, props);
|
|
456
602
|
}
|
|
457
603
|
}
|
|
458
604
|
|
|
@@ -505,8 +651,11 @@ function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseP
|
|
|
505
651
|
instance.props = newProps;
|
|
506
652
|
}
|
|
507
653
|
|
|
508
|
-
//
|
|
509
|
-
|
|
654
|
+
// NOTE: We use a type assertion because @types/react-reconciler (0.28.x) is outdated
|
|
655
|
+
// and doesn't match react-reconciler 0.31.x (React 19). The HostConfig interface
|
|
656
|
+
// has changed significantly - notably commitUpdate no longer receives updatePayload.
|
|
657
|
+
// Once @types/react-reconciler is updated for React 19, we can use proper typing.
|
|
658
|
+
type OurHostConfig = HostConfig<
|
|
510
659
|
string, // Type
|
|
511
660
|
BaseProps, // Props
|
|
512
661
|
Container, // Container
|
|
@@ -514,13 +663,16 @@ export const hostConfig: HostConfig<
|
|
|
514
663
|
TextInstance, // TextInstance
|
|
515
664
|
never, // SuspenseInstance
|
|
516
665
|
never, // HydratableInstance
|
|
517
|
-
|
|
666
|
+
CSObject, // PublicInstance - refs point to the actual UI Toolkit element
|
|
518
667
|
{}, // HostContext
|
|
519
668
|
true, // UpdatePayload (true = needs update)
|
|
520
669
|
ChildSet, // ChildSet
|
|
521
670
|
number, // TimeoutHandle
|
|
522
671
|
number // NoTimeout
|
|
523
|
-
|
|
672
|
+
>;
|
|
673
|
+
|
|
674
|
+
// The host config for react-reconciler
|
|
675
|
+
export const hostConfig = {
|
|
524
676
|
supportsMutation: true,
|
|
525
677
|
supportsPersistence: false,
|
|
526
678
|
supportsHydration: false,
|
|
@@ -528,13 +680,16 @@ export const hostConfig: HostConfig<
|
|
|
528
680
|
isPrimaryRenderer: true,
|
|
529
681
|
noTimeout: -1,
|
|
530
682
|
|
|
531
|
-
createInstance(type, props) {
|
|
683
|
+
createInstance(type: string, props: BaseProps) {
|
|
532
684
|
return createInstance(type, props);
|
|
533
685
|
},
|
|
534
686
|
|
|
535
|
-
createTextInstance(text) {
|
|
536
|
-
// Create a
|
|
537
|
-
|
|
687
|
+
createTextInstance(text: string) {
|
|
688
|
+
// Create a TextElement for implicit text content
|
|
689
|
+
// Using TextElement (not Label) for semantic clarity:
|
|
690
|
+
// - TextElement = raw text content in JSX
|
|
691
|
+
// - Label = explicit <Label> component
|
|
692
|
+
const element = new CS.UnityEngine.UIElements.TextElement();
|
|
538
693
|
element.text = text;
|
|
539
694
|
return {
|
|
540
695
|
element,
|
|
@@ -545,28 +700,46 @@ export const hostConfig: HostConfig<
|
|
|
545
700
|
};
|
|
546
701
|
},
|
|
547
702
|
|
|
548
|
-
appendInitialChild(parentInstance, child) {
|
|
549
|
-
parentInstance
|
|
703
|
+
appendInitialChild(parentInstance: Instance, child: Instance) {
|
|
704
|
+
if (shouldMergeText(parentInstance, child)) {
|
|
705
|
+
appendMergedTextChild(parentInstance, child);
|
|
706
|
+
} else {
|
|
707
|
+
// Non-text child - unmerge any previously merged text first
|
|
708
|
+
handleNonTextChild(parentInstance);
|
|
709
|
+
parentInstance.element.Add(child.element);
|
|
710
|
+
}
|
|
550
711
|
},
|
|
551
712
|
|
|
552
|
-
appendChild(parentInstance, child) {
|
|
553
|
-
parentInstance
|
|
713
|
+
appendChild(parentInstance: Instance, child: Instance) {
|
|
714
|
+
if (shouldMergeText(parentInstance, child)) {
|
|
715
|
+
appendMergedTextChild(parentInstance, child);
|
|
716
|
+
} else {
|
|
717
|
+
// Non-text child - unmerge any previously merged text first
|
|
718
|
+
handleNonTextChild(parentInstance);
|
|
719
|
+
parentInstance.element.Add(child.element);
|
|
720
|
+
}
|
|
554
721
|
},
|
|
555
722
|
|
|
556
|
-
appendChildToContainer(container, child) {
|
|
723
|
+
appendChildToContainer(container: Container, child: Instance) {
|
|
557
724
|
container.Add(child.element);
|
|
558
725
|
},
|
|
559
726
|
|
|
560
|
-
insertBefore(parentInstance, child, beforeChild) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
parentInstance.element.Insert(index, child.element);
|
|
727
|
+
insertBefore(parentInstance: Instance, child: Instance, beforeChild: Instance) {
|
|
728
|
+
if (shouldMergeText(parentInstance, child)) {
|
|
729
|
+
insertMergedTextChild(parentInstance, child, beforeChild);
|
|
564
730
|
} else {
|
|
565
|
-
|
|
731
|
+
// Non-text child - unmerge any previously merged text first
|
|
732
|
+
handleNonTextChild(parentInstance);
|
|
733
|
+
const index = parentInstance.element.IndexOf(beforeChild.element);
|
|
734
|
+
if (index >= 0) {
|
|
735
|
+
parentInstance.element.Insert(index, child.element);
|
|
736
|
+
} else {
|
|
737
|
+
parentInstance.element.Add(child.element);
|
|
738
|
+
}
|
|
566
739
|
}
|
|
567
740
|
},
|
|
568
741
|
|
|
569
|
-
insertInContainerBefore(container, child, beforeChild) {
|
|
742
|
+
insertInContainerBefore(container: Container, child: Instance, beforeChild: Instance) {
|
|
570
743
|
const index = container.IndexOf(beforeChild.element);
|
|
571
744
|
if (index >= 0) {
|
|
572
745
|
container.Insert(index, child.element);
|
|
@@ -575,37 +748,47 @@ export const hostConfig: HostConfig<
|
|
|
575
748
|
}
|
|
576
749
|
},
|
|
577
750
|
|
|
578
|
-
removeChild(parentInstance, child) {
|
|
579
|
-
|
|
580
|
-
|
|
751
|
+
removeChild(parentInstance: Instance, child: Instance) {
|
|
752
|
+
if (child.mergedInto === parentInstance) {
|
|
753
|
+
// Child was merged into parent's text - remove from merged children
|
|
754
|
+
removeMergedTextChild(parentInstance, child);
|
|
755
|
+
} else {
|
|
756
|
+
__eventAPI.removeAllEventListeners(child.element);
|
|
757
|
+
parentInstance.element.Remove(child.element);
|
|
758
|
+
}
|
|
581
759
|
},
|
|
582
760
|
|
|
583
|
-
removeChildFromContainer(container, child) {
|
|
761
|
+
removeChildFromContainer(container: Container, child: Instance) {
|
|
584
762
|
__eventAPI.removeAllEventListeners(child.element);
|
|
585
763
|
container.Remove(child.element);
|
|
586
764
|
},
|
|
587
765
|
|
|
588
|
-
prepareUpdate(_instance, _type, oldProps, newProps) {
|
|
766
|
+
prepareUpdate(_instance: Instance, _type: string, oldProps: BaseProps, newProps: BaseProps) {
|
|
589
767
|
// Return true if we need to update, null if no update needed
|
|
590
768
|
return oldProps !== newProps ? true : null;
|
|
591
769
|
},
|
|
592
770
|
|
|
593
771
|
// React 19 changed the signature: (instance, type, oldProps, newProps, fiber)
|
|
594
772
|
// The updatePayload parameter was removed!
|
|
595
|
-
commitUpdate(instance, _type, oldProps, newProps, _fiber) {
|
|
773
|
+
commitUpdate(instance: Instance, _type: string, oldProps: BaseProps, newProps: BaseProps, _fiber: unknown) {
|
|
596
774
|
updateInstance(instance, oldProps, newProps);
|
|
597
775
|
},
|
|
598
776
|
|
|
599
|
-
commitTextUpdate(textInstance, _oldText, newText) {
|
|
777
|
+
commitTextUpdate(textInstance: Instance, _oldText: string, newText: string) {
|
|
600
778
|
textInstance.element.text = newText;
|
|
779
|
+
// If this text is merged into a parent, rebuild the parent's concatenated text
|
|
780
|
+
if (textInstance.mergedInto) {
|
|
781
|
+
rebuildMergedText(textInstance.mergedInto);
|
|
782
|
+
}
|
|
601
783
|
},
|
|
602
784
|
|
|
603
785
|
finalizeInitialChildren() {
|
|
604
786
|
return false;
|
|
605
787
|
},
|
|
606
788
|
|
|
607
|
-
getPublicInstance(instance) {
|
|
608
|
-
|
|
789
|
+
getPublicInstance(instance: Instance) {
|
|
790
|
+
// Return the actual UI Toolkit element so refs point to it
|
|
791
|
+
return instance.element;
|
|
609
792
|
},
|
|
610
793
|
|
|
611
794
|
prepareForCommit() {
|
|
@@ -624,7 +807,7 @@ export const hostConfig: HostConfig<
|
|
|
624
807
|
return {};
|
|
625
808
|
},
|
|
626
809
|
|
|
627
|
-
getChildHostContext(parentHostContext) {
|
|
810
|
+
getChildHostContext(parentHostContext: {}) {
|
|
628
811
|
return parentHostContext;
|
|
629
812
|
},
|
|
630
813
|
|
|
@@ -632,7 +815,7 @@ export const hostConfig: HostConfig<
|
|
|
632
815
|
return false;
|
|
633
816
|
},
|
|
634
817
|
|
|
635
|
-
clearContainer(container) {
|
|
818
|
+
clearContainer(container: Container) {
|
|
636
819
|
container.Clear();
|
|
637
820
|
},
|
|
638
821
|
|
|
@@ -746,4 +929,4 @@ export const hostConfig: HostConfig<
|
|
|
746
929
|
bindToConsole(methodName: string, args: unknown[], _badgeName: string) {
|
|
747
930
|
return (console as Record<string, Function>)[methodName]?.bind(console, ...args);
|
|
748
931
|
},
|
|
749
|
-
};
|
|
932
|
+
} as unknown as OurHostConfig;
|