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.
@@ -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
- // Setting to undefined clears the inline style, falling back to USS
273
- s[key] = undefined;
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
- // Apply component-specific props
338
- function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>) {
339
- // For Label, Button - set text property directly
340
- if (props.text !== undefined) {
341
- (element as { text: string }).text = props.text as string;
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
- // For TextField, Toggle, Slider - set value property
344
- if (props.value !== undefined) {
345
- (element as { value: unknown }).value = props.value;
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
- // For input elements that have a label
348
- if (props.label !== undefined) {
349
- (element as { label: string }).label = props.label as string;
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
- // ScrollView-specific properties
353
- if (type === 'ojs-scrollview') {
354
- const sv = element as CSScrollView;
355
- if (props.mode !== undefined) {
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
- // ListView-specific properties
391
- if (type === 'ojs-listview') {
392
- const lv = element as CSListView;
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
- // Data binding - these are the core callbacks
395
- if (props.itemsSource !== undefined) {
396
- lv.itemsSource = props.itemsSource as unknown[];
397
- }
398
- if (props.makeItem !== undefined) {
399
- lv.makeItem = props.makeItem as () => CSObject;
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
- // Virtualization
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
- // Selection
420
- if (props.selectionType !== undefined) {
421
- lv.selectionType = CS.UnityEngine.UIElements.SelectionType[props.selectionType as string];
422
- }
423
- if (props.selectedIndex !== undefined) {
424
- lv.selectedIndex = props.selectedIndex as number;
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
- // Reordering
431
- if (props.reorderable !== undefined) {
432
- lv.reorderable = props.reorderable as boolean;
433
- }
434
- if (props.reorderMode !== undefined) {
435
- lv.reorderMode = CS.UnityEngine.UIElements.ListViewReorderMode[props.reorderMode as string];
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
- // Header/Footer
439
- if (props.showFoldoutHeader !== undefined) {
440
- lv.showFoldoutHeader = props.showFoldoutHeader as boolean;
441
- }
442
- if (props.headerTitle !== undefined) {
443
- lv.headerTitle = props.headerTitle as string;
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
- // Appearance
450
- if (props.showBorder !== undefined) {
451
- lv.showBorder = props.showBorder as boolean;
452
- }
453
- if (props.showAlternatingRowBackgrounds !== undefined) {
454
- lv.showAlternatingRowBackgrounds = CS.UnityEngine.UIElements.AlternatingRowBackground[props.showAlternatingRowBackgrounds as string];
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
- // The host config for react-reconciler
509
- export const hostConfig: HostConfig<
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
- Instance, // PublicInstance
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 Label for text content
537
- const element = new CS.UnityEngine.UIElements.Label();
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.element.Add(child.element);
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.element.Add(child.element);
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
- const index = parentInstance.element.IndexOf(beforeChild.element);
562
- if (index >= 0) {
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
- parentInstance.element.Add(child.element);
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
- __eventAPI.removeAllEventListeners(child.element);
580
- parentInstance.element.Remove(child.element);
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
- return instance;
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;