onejs-react 0.1.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/package.json +43 -0
- package/src/__tests__/components.test.tsx +388 -0
- package/src/__tests__/host-config.test.ts +674 -0
- package/src/__tests__/mocks.ts +311 -0
- package/src/__tests__/renderer.test.tsx +387 -0
- package/src/__tests__/setup.ts +52 -0
- package/src/__tests__/style-parser.test.ts +321 -0
- package/src/components.tsx +87 -0
- package/src/host-config.ts +749 -0
- package/src/index.ts +54 -0
- package/src/renderer.ts +73 -0
- package/src/screen.tsx +242 -0
- package/src/style-parser.ts +288 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import type {HostConfig} from 'react-reconciler';
|
|
2
|
+
import type {BaseProps, ViewStyle} from './types';
|
|
3
|
+
import {parseStyleValue} from './style-parser';
|
|
4
|
+
|
|
5
|
+
// Global declarations for QuickJS environment
|
|
6
|
+
declare function setTimeout(callback: () => void, ms?: number): number;
|
|
7
|
+
|
|
8
|
+
declare function clearTimeout(id: number): void;
|
|
9
|
+
|
|
10
|
+
declare const console: { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
|
|
11
|
+
|
|
12
|
+
// Priority constants from react-reconciler/constants
|
|
13
|
+
// These match React's internal lane priorities
|
|
14
|
+
const DiscreteEventPriority = 2;
|
|
15
|
+
const ContinuousEventPriority = 8;
|
|
16
|
+
const DefaultEventPriority = 32;
|
|
17
|
+
const IdleEventPriority = 536870912;
|
|
18
|
+
|
|
19
|
+
// Current update priority - used by React's scheduler
|
|
20
|
+
let currentUpdatePriority = DefaultEventPriority;
|
|
21
|
+
|
|
22
|
+
// Microtask scheduling
|
|
23
|
+
declare function queueMicrotask(callback: () => void): void;
|
|
24
|
+
|
|
25
|
+
// Unity enum types (accessed as CS.UnityEngine.UIElements.EnumName.Value)
|
|
26
|
+
interface CSEnum {
|
|
27
|
+
[key: string]: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// CS interop - these are provided by QuickJSBootstrap.js
|
|
31
|
+
declare const CS: {
|
|
32
|
+
UnityEngine: {
|
|
33
|
+
UIElements: {
|
|
34
|
+
VisualElement: new () => CSObject;
|
|
35
|
+
Label: new () => CSObject;
|
|
36
|
+
Button: new () => CSObject;
|
|
37
|
+
TextField: new () => CSObject;
|
|
38
|
+
Toggle: new () => CSObject;
|
|
39
|
+
Slider: new () => CSObject;
|
|
40
|
+
ScrollView: new () => CSObject;
|
|
41
|
+
Image: new () => CSObject;
|
|
42
|
+
ListView: new () => CSListView;
|
|
43
|
+
// Enums
|
|
44
|
+
ScrollViewMode: CSEnum;
|
|
45
|
+
ScrollerVisibility: CSEnum;
|
|
46
|
+
TouchScrollBehavior: CSEnum;
|
|
47
|
+
NestedInteractionKind: CSEnum;
|
|
48
|
+
SelectionType: CSEnum;
|
|
49
|
+
ListViewReorderMode: CSEnum;
|
|
50
|
+
AlternatingRowBackground: CSEnum;
|
|
51
|
+
CollectionVirtualizationMethod: CSEnum;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
declare const __eventAPI: {
|
|
57
|
+
addEventListener: (element: CSObject, eventType: string, callback: Function) => void;
|
|
58
|
+
removeEventListener: (element: CSObject, eventType: string, callback: Function) => void;
|
|
59
|
+
removeAllEventListeners: (element: CSObject) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
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
|
+
interface CSStyle {
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ScrollView-specific interface
|
|
85
|
+
interface CSScrollView extends CSObject {
|
|
86
|
+
mode: number;
|
|
87
|
+
horizontalScrollerVisibility: number;
|
|
88
|
+
verticalScrollerVisibility: number;
|
|
89
|
+
elasticity: number;
|
|
90
|
+
elasticAnimationIntervalMs: number;
|
|
91
|
+
scrollDecelerationRate: number;
|
|
92
|
+
mouseWheelScrollSize: number;
|
|
93
|
+
horizontalPageSize: number;
|
|
94
|
+
verticalPageSize: number;
|
|
95
|
+
touchScrollBehavior: number;
|
|
96
|
+
nestedInteractionKind: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ListView-specific interface
|
|
100
|
+
interface CSListView extends CSObject {
|
|
101
|
+
// Data binding callbacks
|
|
102
|
+
itemsSource: unknown[];
|
|
103
|
+
makeItem: () => CSObject;
|
|
104
|
+
bindItem: (element: CSObject, index: number) => void;
|
|
105
|
+
unbindItem: (element: CSObject, index: number) => void;
|
|
106
|
+
destroyItem: (element: CSObject) => void;
|
|
107
|
+
|
|
108
|
+
// Virtualization
|
|
109
|
+
fixedItemHeight: number;
|
|
110
|
+
virtualizationMethod: number;
|
|
111
|
+
|
|
112
|
+
// Selection
|
|
113
|
+
selectionType: number;
|
|
114
|
+
selectedIndex: number;
|
|
115
|
+
selectedIndices: number[];
|
|
116
|
+
|
|
117
|
+
// Reordering
|
|
118
|
+
reorderable: boolean;
|
|
119
|
+
reorderMode: number;
|
|
120
|
+
|
|
121
|
+
// Header/Footer
|
|
122
|
+
showFoldoutHeader: boolean;
|
|
123
|
+
headerTitle: string;
|
|
124
|
+
showAddRemoveFooter: boolean;
|
|
125
|
+
|
|
126
|
+
// Appearance
|
|
127
|
+
showBorder: boolean;
|
|
128
|
+
showAlternatingRowBackgrounds: number;
|
|
129
|
+
|
|
130
|
+
// Methods
|
|
131
|
+
RefreshItems: () => void;
|
|
132
|
+
Rebuild: () => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Instance type used by the reconciler
|
|
136
|
+
export interface Instance {
|
|
137
|
+
element: CSObject;
|
|
138
|
+
type: string;
|
|
139
|
+
props: BaseProps;
|
|
140
|
+
eventHandlers: Map<string, Function>;
|
|
141
|
+
appliedStyleKeys: Set<string>; // Track which style properties are currently applied
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type TextInstance = Instance; // For Label elements with text content
|
|
145
|
+
export type Container = CSObject;
|
|
146
|
+
export type ChildSet = never; // Not using persistent mode
|
|
147
|
+
|
|
148
|
+
// Map React element types to UI Toolkit classes
|
|
149
|
+
// Element types use 'ojs-' prefix to avoid conflicts with HTML/SVG in @types/react
|
|
150
|
+
const TYPE_MAP: Record<string, () => CSObject> = {
|
|
151
|
+
'ojs-view': () => new CS.UnityEngine.UIElements.VisualElement(),
|
|
152
|
+
'ojs-label': () => new CS.UnityEngine.UIElements.Label(),
|
|
153
|
+
'ojs-button': () => new CS.UnityEngine.UIElements.Button(),
|
|
154
|
+
'ojs-textfield': () => new CS.UnityEngine.UIElements.TextField(),
|
|
155
|
+
'ojs-toggle': () => new CS.UnityEngine.UIElements.Toggle(),
|
|
156
|
+
'ojs-slider': () => new CS.UnityEngine.UIElements.Slider(),
|
|
157
|
+
'ojs-scrollview': () => new CS.UnityEngine.UIElements.ScrollView(),
|
|
158
|
+
'ojs-image': () => new CS.UnityEngine.UIElements.Image(),
|
|
159
|
+
'ojs-listview': () => new CS.UnityEngine.UIElements.ListView(),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Event prop to event type mapping
|
|
163
|
+
const EVENT_PROPS: Record<string, string> = {
|
|
164
|
+
onClick: 'click',
|
|
165
|
+
onPointerDown: 'pointerdown',
|
|
166
|
+
onPointerUp: 'pointerup',
|
|
167
|
+
onPointerMove: 'pointermove',
|
|
168
|
+
onPointerEnter: 'pointerenter',
|
|
169
|
+
onPointerLeave: 'pointerleave',
|
|
170
|
+
onFocus: 'focus',
|
|
171
|
+
onBlur: 'blur',
|
|
172
|
+
onKeyDown: 'keydown',
|
|
173
|
+
onKeyUp: 'keyup',
|
|
174
|
+
onChange: 'change',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Shorthand style properties that expand to multiple properties
|
|
178
|
+
const STYLE_SHORTHANDS: Record<string, string[]> = {
|
|
179
|
+
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
180
|
+
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
181
|
+
borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
|
|
182
|
+
borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
|
|
183
|
+
borderRadius: ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Character escape mapping for Tailwind/USS compatibility
|
|
187
|
+
// USS class names only support [\w-] so special characters must be escaped
|
|
188
|
+
const CLASS_ESCAPE_MAP: [string, string][] = [
|
|
189
|
+
[':', '_c_'], // Pseudo-classes and breakpoints (hover:, sm:)
|
|
190
|
+
['/', '_s_'], // Fractions (w-1/2)
|
|
191
|
+
['.', '_d_'], // Decimals (p-2.5)
|
|
192
|
+
['[', '_lb_'], // Arbitrary values ([100px])
|
|
193
|
+
[']', '_rb_'],
|
|
194
|
+
['(', '_lp_'], // Functions (calc())
|
|
195
|
+
[')', '_rp_'],
|
|
196
|
+
['#', '_n_'], // Hex colors
|
|
197
|
+
['%', '_p_'], // Percentages
|
|
198
|
+
[',', '_cm_'], // Multiple values
|
|
199
|
+
['&', '_amp_'],
|
|
200
|
+
['>', '_gt_'],
|
|
201
|
+
['<', '_lt_'],
|
|
202
|
+
['*', '_ast_'],
|
|
203
|
+
["'", '_sq_'],
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Escape special characters in a class name for USS compatibility
|
|
208
|
+
* Tailwind class names like "hover:bg-red-500" become "hover_c_bg-red-500"
|
|
209
|
+
*/
|
|
210
|
+
function escapeClassName(name: string): string {
|
|
211
|
+
let escaped = name;
|
|
212
|
+
for (const [char, replacement] of CLASS_ESCAPE_MAP) {
|
|
213
|
+
if (escaped.includes(char)) {
|
|
214
|
+
escaped = escaped.split(char).join(replacement);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return escaped;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get all expanded property keys for a style object
|
|
221
|
+
function getExpandedStyleKeys(style: ViewStyle | undefined): Set<string> {
|
|
222
|
+
const keys = new Set<string>();
|
|
223
|
+
if (!style) return keys;
|
|
224
|
+
|
|
225
|
+
for (const [key, value] of Object.entries(style)) {
|
|
226
|
+
if (value === undefined) continue;
|
|
227
|
+
|
|
228
|
+
const expanded = STYLE_SHORTHANDS[key];
|
|
229
|
+
if (expanded) {
|
|
230
|
+
for (const prop of expanded) {
|
|
231
|
+
keys.add(prop);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
keys.add(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return keys;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Apply style properties to element, returns the set of applied keys
|
|
241
|
+
function applyStyle(element: CSObject, style: ViewStyle | undefined): Set<string> {
|
|
242
|
+
const appliedKeys = new Set<string>();
|
|
243
|
+
if (!style) return appliedKeys;
|
|
244
|
+
|
|
245
|
+
const s = element.style;
|
|
246
|
+
for (const [key, value] of Object.entries(style)) {
|
|
247
|
+
if (value === undefined) continue;
|
|
248
|
+
|
|
249
|
+
// Handle shorthand properties
|
|
250
|
+
const expanded = STYLE_SHORTHANDS[key];
|
|
251
|
+
if (expanded) {
|
|
252
|
+
// Parse the value once, apply to all expanded properties
|
|
253
|
+
const parsed = parseStyleValue(expanded[0], value);
|
|
254
|
+
for (const prop of expanded) {
|
|
255
|
+
s[prop] = parsed;
|
|
256
|
+
appliedKeys.add(prop);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// Parse and apply individual property
|
|
260
|
+
s[key] = parseStyleValue(key, value);
|
|
261
|
+
appliedKeys.add(key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return appliedKeys;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Clear style properties that are no longer in the new style
|
|
268
|
+
function clearRemovedStyles(element: CSObject, oldKeys: Set<string>, newKeys: Set<string>) {
|
|
269
|
+
const s = element.style;
|
|
270
|
+
for (const key of oldKeys) {
|
|
271
|
+
if (!newKeys.has(key)) {
|
|
272
|
+
// Setting to undefined clears the inline style, falling back to USS
|
|
273
|
+
s[key] = undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Parse className string into a Set of escaped class names
|
|
279
|
+
function parseClassNames(className: string | undefined): Set<string> {
|
|
280
|
+
if (!className) return new Set();
|
|
281
|
+
return new Set(
|
|
282
|
+
className.split(/\s+/)
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.map(escapeClassName)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Apply className(s) to element (with escaping for Tailwind/USS compatibility)
|
|
289
|
+
function applyClassName(element: CSObject, className: string | undefined) {
|
|
290
|
+
if (!className) return;
|
|
291
|
+
|
|
292
|
+
const classes = className.split(/\s+/).filter(Boolean);
|
|
293
|
+
for (const cls of classes) {
|
|
294
|
+
element.AddToClassList(escapeClassName(cls));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update className selectively - only add/remove what changed
|
|
299
|
+
function updateClassNames(element: CSObject, oldClassName: string | undefined, newClassName: string | undefined) {
|
|
300
|
+
const oldClasses = parseClassNames(oldClassName);
|
|
301
|
+
const newClasses = parseClassNames(newClassName);
|
|
302
|
+
|
|
303
|
+
// Remove classes that are no longer present
|
|
304
|
+
for (const cls of oldClasses) {
|
|
305
|
+
if (!newClasses.has(cls)) {
|
|
306
|
+
element.RemoveFromClassList(cls);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Add classes that are new
|
|
311
|
+
for (const cls of newClasses) {
|
|
312
|
+
if (!oldClasses.has(cls)) {
|
|
313
|
+
element.AddToClassList(cls);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Apply event handlers
|
|
319
|
+
function applyEvents(instance: Instance, props: BaseProps) {
|
|
320
|
+
for (const [propName, eventType] of Object.entries(EVENT_PROPS)) {
|
|
321
|
+
const handler = (props as Record<string, unknown>)[propName] as Function | undefined;
|
|
322
|
+
const existingHandler = instance.eventHandlers.get(eventType);
|
|
323
|
+
|
|
324
|
+
if (handler !== existingHandler) {
|
|
325
|
+
if (existingHandler) {
|
|
326
|
+
__eventAPI.removeEventListener(instance.element, eventType, existingHandler);
|
|
327
|
+
instance.eventHandlers.delete(eventType);
|
|
328
|
+
}
|
|
329
|
+
if (handler) {
|
|
330
|
+
__eventAPI.addEventListener(instance.element, eventType, handler);
|
|
331
|
+
instance.eventHandlers.set(eventType, handler);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
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;
|
|
342
|
+
}
|
|
343
|
+
// For TextField, Toggle, Slider - set value property
|
|
344
|
+
if (props.value !== undefined) {
|
|
345
|
+
(element as { value: unknown }).value = props.value;
|
|
346
|
+
}
|
|
347
|
+
// For input elements that have a label
|
|
348
|
+
if (props.label !== undefined) {
|
|
349
|
+
(element as { label: string }).label = props.label as string;
|
|
350
|
+
}
|
|
351
|
+
|
|
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
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ListView-specific properties
|
|
391
|
+
if (type === 'ojs-listview') {
|
|
392
|
+
const lv = element as CSListView;
|
|
393
|
+
|
|
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;
|
|
409
|
+
}
|
|
410
|
+
|
|
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
|
+
}
|
|
418
|
+
|
|
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
|
+
}
|
|
429
|
+
|
|
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
|
+
}
|
|
437
|
+
|
|
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
|
+
}
|
|
448
|
+
|
|
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
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Create an instance
|
|
460
|
+
function createInstance(type: string, props: BaseProps): Instance {
|
|
461
|
+
const factory = TYPE_MAP[type];
|
|
462
|
+
if (!factory) {
|
|
463
|
+
throw new Error(`Unknown element type: ${type}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const element = factory();
|
|
467
|
+
const appliedStyleKeys = applyStyle(element, props.style);
|
|
468
|
+
const instance: Instance = {
|
|
469
|
+
element,
|
|
470
|
+
type,
|
|
471
|
+
props,
|
|
472
|
+
eventHandlers: new Map(),
|
|
473
|
+
appliedStyleKeys,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
applyClassName(element, props.className);
|
|
477
|
+
applyEvents(instance, props);
|
|
478
|
+
applyComponentProps(element, type, props as Record<string, unknown>);
|
|
479
|
+
|
|
480
|
+
return instance;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Update an instance with new props
|
|
484
|
+
function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseProps) {
|
|
485
|
+
const element = instance.element;
|
|
486
|
+
|
|
487
|
+
// Update style - clear removed properties, then apply new ones
|
|
488
|
+
if (oldProps.style !== newProps.style) {
|
|
489
|
+
const newStyleKeys = getExpandedStyleKeys(newProps.style);
|
|
490
|
+
clearRemovedStyles(element, instance.appliedStyleKeys, newStyleKeys);
|
|
491
|
+
instance.appliedStyleKeys = applyStyle(element, newProps.style);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Update className - selectively add/remove classes
|
|
495
|
+
if (oldProps.className !== newProps.className) {
|
|
496
|
+
updateClassNames(element, oldProps.className, newProps.className);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Update events
|
|
500
|
+
applyEvents(instance, newProps);
|
|
501
|
+
|
|
502
|
+
// Update component-specific props
|
|
503
|
+
applyComponentProps(element, instance.type, newProps as Record<string, unknown>);
|
|
504
|
+
|
|
505
|
+
instance.props = newProps;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// The host config for react-reconciler
|
|
509
|
+
export const hostConfig: HostConfig<
|
|
510
|
+
string, // Type
|
|
511
|
+
BaseProps, // Props
|
|
512
|
+
Container, // Container
|
|
513
|
+
Instance, // Instance
|
|
514
|
+
TextInstance, // TextInstance
|
|
515
|
+
never, // SuspenseInstance
|
|
516
|
+
never, // HydratableInstance
|
|
517
|
+
Instance, // PublicInstance
|
|
518
|
+
{}, // HostContext
|
|
519
|
+
true, // UpdatePayload (true = needs update)
|
|
520
|
+
ChildSet, // ChildSet
|
|
521
|
+
number, // TimeoutHandle
|
|
522
|
+
number // NoTimeout
|
|
523
|
+
> = {
|
|
524
|
+
supportsMutation: true,
|
|
525
|
+
supportsPersistence: false,
|
|
526
|
+
supportsHydration: false,
|
|
527
|
+
|
|
528
|
+
isPrimaryRenderer: true,
|
|
529
|
+
noTimeout: -1,
|
|
530
|
+
|
|
531
|
+
createInstance(type, props) {
|
|
532
|
+
return createInstance(type, props);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
createTextInstance(text) {
|
|
536
|
+
// Create a Label for text content
|
|
537
|
+
const element = new CS.UnityEngine.UIElements.Label();
|
|
538
|
+
element.text = text;
|
|
539
|
+
return {
|
|
540
|
+
element,
|
|
541
|
+
type: 'text',
|
|
542
|
+
props: {},
|
|
543
|
+
eventHandlers: new Map(),
|
|
544
|
+
appliedStyleKeys: new Set(),
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
appendInitialChild(parentInstance, child) {
|
|
549
|
+
parentInstance.element.Add(child.element);
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
appendChild(parentInstance, child) {
|
|
553
|
+
parentInstance.element.Add(child.element);
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
appendChildToContainer(container, child) {
|
|
557
|
+
container.Add(child.element);
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
insertBefore(parentInstance, child, beforeChild) {
|
|
561
|
+
const index = parentInstance.element.IndexOf(beforeChild.element);
|
|
562
|
+
if (index >= 0) {
|
|
563
|
+
parentInstance.element.Insert(index, child.element);
|
|
564
|
+
} else {
|
|
565
|
+
parentInstance.element.Add(child.element);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
insertInContainerBefore(container, child, beforeChild) {
|
|
570
|
+
const index = container.IndexOf(beforeChild.element);
|
|
571
|
+
if (index >= 0) {
|
|
572
|
+
container.Insert(index, child.element);
|
|
573
|
+
} else {
|
|
574
|
+
container.Add(child.element);
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
removeChild(parentInstance, child) {
|
|
579
|
+
__eventAPI.removeAllEventListeners(child.element);
|
|
580
|
+
parentInstance.element.Remove(child.element);
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
removeChildFromContainer(container, child) {
|
|
584
|
+
__eventAPI.removeAllEventListeners(child.element);
|
|
585
|
+
container.Remove(child.element);
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
prepareUpdate(_instance, _type, oldProps, newProps) {
|
|
589
|
+
// Return true if we need to update, null if no update needed
|
|
590
|
+
return oldProps !== newProps ? true : null;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
// React 19 changed the signature: (instance, type, oldProps, newProps, fiber)
|
|
594
|
+
// The updatePayload parameter was removed!
|
|
595
|
+
commitUpdate(instance, _type, oldProps, newProps, _fiber) {
|
|
596
|
+
updateInstance(instance, oldProps, newProps);
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
commitTextUpdate(textInstance, _oldText, newText) {
|
|
600
|
+
textInstance.element.text = newText;
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
finalizeInitialChildren() {
|
|
604
|
+
return false;
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
getPublicInstance(instance) {
|
|
608
|
+
return instance;
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
prepareForCommit() {
|
|
612
|
+
return null;
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
resetAfterCommit() {
|
|
616
|
+
// Nothing to do
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
preparePortalMount() {
|
|
620
|
+
// Nothing to do
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
getRootHostContext() {
|
|
624
|
+
return {};
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
getChildHostContext(parentHostContext) {
|
|
628
|
+
return parentHostContext;
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
shouldSetTextContent() {
|
|
632
|
+
return false;
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
clearContainer(container) {
|
|
636
|
+
container.Clear();
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
scheduleTimeout: setTimeout,
|
|
640
|
+
cancelTimeout: clearTimeout,
|
|
641
|
+
|
|
642
|
+
// Priority management - required by React 19's reconciler
|
|
643
|
+
setCurrentUpdatePriority(priority: number) {
|
|
644
|
+
currentUpdatePriority = priority;
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
getCurrentUpdatePriority() {
|
|
648
|
+
return currentUpdatePriority;
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
resolveUpdatePriority() {
|
|
652
|
+
// When no specific priority is set, use default
|
|
653
|
+
return currentUpdatePriority || DefaultEventPriority;
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
getCurrentEventPriority() {
|
|
657
|
+
return DefaultEventPriority;
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
// Microtask support
|
|
661
|
+
supportsMicrotasks: true,
|
|
662
|
+
scheduleMicrotask: queueMicrotask,
|
|
663
|
+
|
|
664
|
+
// Transition support
|
|
665
|
+
shouldAttemptEagerTransition() {
|
|
666
|
+
return false;
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
// Form support (React 19)
|
|
670
|
+
NotPendingTransition: null as unknown,
|
|
671
|
+
resetFormInstance() {},
|
|
672
|
+
|
|
673
|
+
getInstanceFromNode() {
|
|
674
|
+
return null;
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
beforeActiveInstanceBlur() {
|
|
678
|
+
},
|
|
679
|
+
afterActiveInstanceBlur() {
|
|
680
|
+
},
|
|
681
|
+
prepareScopeUpdate() {
|
|
682
|
+
},
|
|
683
|
+
getInstanceFromScope() {
|
|
684
|
+
return null;
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
detachDeletedInstance() {
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
// Suspense commit support (React 19)
|
|
691
|
+
maySuspendCommit() {
|
|
692
|
+
return false;
|
|
693
|
+
},
|
|
694
|
+
preloadInstance() {
|
|
695
|
+
return true; // Already loaded
|
|
696
|
+
},
|
|
697
|
+
startSuspendingCommit() {
|
|
698
|
+
},
|
|
699
|
+
suspendInstance() {
|
|
700
|
+
},
|
|
701
|
+
waitForCommitToBeReady() {
|
|
702
|
+
return null;
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
// Visibility support
|
|
706
|
+
hideInstance(instance: Instance) {
|
|
707
|
+
instance.element.style.display = 'none';
|
|
708
|
+
},
|
|
709
|
+
hideTextInstance(textInstance: TextInstance) {
|
|
710
|
+
textInstance.element.style.display = 'none';
|
|
711
|
+
},
|
|
712
|
+
unhideInstance(instance: Instance, _props: BaseProps) {
|
|
713
|
+
instance.element.style.display = '';
|
|
714
|
+
},
|
|
715
|
+
unhideTextInstance(textInstance: TextInstance, _text: string) {
|
|
716
|
+
textInstance.element.style.display = '';
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
// Text content
|
|
720
|
+
resetTextContent(_instance: Instance) {
|
|
721
|
+
// Nothing to do for UI Toolkit
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
// Resources (not used)
|
|
725
|
+
supportsResources: false,
|
|
726
|
+
|
|
727
|
+
// Singletons (not used)
|
|
728
|
+
supportsSingletons: false,
|
|
729
|
+
|
|
730
|
+
// Test selectors (not used)
|
|
731
|
+
supportsTestSelectors: false,
|
|
732
|
+
|
|
733
|
+
// Post paint callback (not used)
|
|
734
|
+
requestPostPaintCallback() {
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
// Event resolution (not used)
|
|
738
|
+
resolveEventType() {
|
|
739
|
+
return null;
|
|
740
|
+
},
|
|
741
|
+
resolveEventTimeStamp() {
|
|
742
|
+
return 0;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
// Console binding (dev tools)
|
|
746
|
+
bindToConsole(methodName: string, args: unknown[], _badgeName: string) {
|
|
747
|
+
return (console as Record<string, Function>)[methodName]?.bind(console, ...args);
|
|
748
|
+
},
|
|
749
|
+
};
|