onejs-react 0.1.3 → 0.1.5
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 +1 -1
- package/src/hooks.ts +216 -0
- package/src/host-config.ts +42 -5
- package/src/index.ts +3 -0
- package/src/types.ts +8 -0
package/package.json
CHANGED
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syncs a value from C# (or any external source) to React state, checking every frame.
|
|
5
|
+
*
|
|
6
|
+
* This hook eliminates the need for C# events or codegen - just read any property
|
|
7
|
+
* and React will automatically update when it changes.
|
|
8
|
+
*
|
|
9
|
+
* @param getter - Function that returns the current value (called every frame)
|
|
10
|
+
* @param deps - Optional dependency array (if getter depends on changing references)
|
|
11
|
+
* @returns The current value, updated each frame if changed
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Sync a C# property to React
|
|
15
|
+
* const health = useFrameSync(() => player.health)
|
|
16
|
+
* const position = useFrameSync(() => transform.position)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With dependencies (if the object reference can change)
|
|
20
|
+
* const health = useFrameSync(() => currentPlayer.health, [currentPlayer])
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Derived values work too
|
|
24
|
+
* const healthPercent = useFrameSync(() => player.health / player.maxHealth * 100)
|
|
25
|
+
*/
|
|
26
|
+
export function useFrameSync<T>(getter: () => T, deps: readonly unknown[] = []): T {
|
|
27
|
+
// Safely get initial value
|
|
28
|
+
const getInitialValue = (): T => {
|
|
29
|
+
try {
|
|
30
|
+
return getter()
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined as T
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
37
|
+
const lastValueRef = useRef<T>(value)
|
|
38
|
+
const getterRef = useRef(getter)
|
|
39
|
+
const runningRef = useRef(false)
|
|
40
|
+
|
|
41
|
+
// Keep getter ref updated
|
|
42
|
+
getterRef.current = getter
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// Re-initialize when deps change
|
|
46
|
+
try {
|
|
47
|
+
const initial = getterRef.current()
|
|
48
|
+
lastValueRef.current = initial
|
|
49
|
+
setValue(initial)
|
|
50
|
+
} catch {
|
|
51
|
+
// Getter failed, keep current value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
runningRef.current = true
|
|
55
|
+
|
|
56
|
+
const check = () => {
|
|
57
|
+
if (!runningRef.current) return
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const current = getterRef.current()
|
|
61
|
+
if (!Object.is(current, lastValueRef.current)) {
|
|
62
|
+
lastValueRef.current = current
|
|
63
|
+
setValue(current)
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Getter might fail if object was destroyed - that's ok
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (runningRef.current) {
|
|
70
|
+
requestAnimationFrame(check)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
requestAnimationFrame(check)
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
runningRef.current = false
|
|
78
|
+
}
|
|
79
|
+
}, deps)
|
|
80
|
+
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Similar to useFrameSync but with a custom equality function.
|
|
86
|
+
* Useful for objects/structs where reference equality isn't sufficient.
|
|
87
|
+
*
|
|
88
|
+
* @param getter - Function that returns the current value
|
|
89
|
+
* @param isEqual - Custom equality function
|
|
90
|
+
* @param deps - Optional dependency array
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Sync a Vector3, comparing by value not reference
|
|
94
|
+
* const pos = useFrameSyncWith(
|
|
95
|
+
* () => transform.position,
|
|
96
|
+
* (a, b) => a.x === b.x && a.y === b.y && a.z === b.z
|
|
97
|
+
* )
|
|
98
|
+
*/
|
|
99
|
+
export function useFrameSyncWith<T>(
|
|
100
|
+
getter: () => T,
|
|
101
|
+
isEqual: (a: T, b: T) => boolean,
|
|
102
|
+
deps: readonly unknown[] = []
|
|
103
|
+
): T {
|
|
104
|
+
const getInitialValue = (): T => {
|
|
105
|
+
try {
|
|
106
|
+
return getter()
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined as T
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
113
|
+
const lastValueRef = useRef<T>(value)
|
|
114
|
+
const getterRef = useRef(getter)
|
|
115
|
+
const isEqualRef = useRef(isEqual)
|
|
116
|
+
const runningRef = useRef(false)
|
|
117
|
+
|
|
118
|
+
getterRef.current = getter
|
|
119
|
+
isEqualRef.current = isEqual
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
try {
|
|
123
|
+
const initial = getterRef.current()
|
|
124
|
+
lastValueRef.current = initial
|
|
125
|
+
setValue(initial)
|
|
126
|
+
} catch {
|
|
127
|
+
// Getter failed, keep current value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
runningRef.current = true
|
|
131
|
+
|
|
132
|
+
const check = () => {
|
|
133
|
+
if (!runningRef.current) return
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const current = getterRef.current()
|
|
137
|
+
if (!isEqualRef.current(current, lastValueRef.current)) {
|
|
138
|
+
lastValueRef.current = current
|
|
139
|
+
setValue(current)
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Getter might fail if object was destroyed
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (runningRef.current) {
|
|
146
|
+
requestAnimationFrame(check)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
requestAnimationFrame(check)
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
runningRef.current = false
|
|
154
|
+
}
|
|
155
|
+
}, deps)
|
|
156
|
+
|
|
157
|
+
return value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Throttled version of useFrameSync that only checks at a specified interval.
|
|
162
|
+
* Useful when you don't need per-frame updates and want to reduce overhead.
|
|
163
|
+
*
|
|
164
|
+
* @param getter - Function that returns the current value
|
|
165
|
+
* @param intervalMs - How often to check for changes (in milliseconds)
|
|
166
|
+
* @param deps - Optional dependency array
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Check every 100ms instead of every frame
|
|
170
|
+
* const score = useThrottledSync(() => gameState.score, 100)
|
|
171
|
+
*/
|
|
172
|
+
export function useThrottledSync<T>(
|
|
173
|
+
getter: () => T,
|
|
174
|
+
intervalMs: number,
|
|
175
|
+
deps: readonly unknown[] = []
|
|
176
|
+
): T {
|
|
177
|
+
const getInitialValue = (): T => {
|
|
178
|
+
try {
|
|
179
|
+
return getter()
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined as T
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
186
|
+
const lastValueRef = useRef<T>(value)
|
|
187
|
+
const getterRef = useRef(getter)
|
|
188
|
+
|
|
189
|
+
getterRef.current = getter
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
try {
|
|
193
|
+
const initial = getterRef.current()
|
|
194
|
+
lastValueRef.current = initial
|
|
195
|
+
setValue(initial)
|
|
196
|
+
} catch {
|
|
197
|
+
// Getter failed, keep current value
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const id = setInterval(() => {
|
|
201
|
+
try {
|
|
202
|
+
const current = getterRef.current()
|
|
203
|
+
if (!Object.is(current, lastValueRef.current)) {
|
|
204
|
+
lastValueRef.current = current
|
|
205
|
+
setValue(current)
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Getter might fail if object was destroyed
|
|
209
|
+
}
|
|
210
|
+
}, intervalMs)
|
|
211
|
+
|
|
212
|
+
return () => clearInterval(id)
|
|
213
|
+
}, [...deps, intervalMs])
|
|
214
|
+
|
|
215
|
+
return value
|
|
216
|
+
}
|
package/src/host-config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {BaseProps, ViewStyle, VisualElement, GenerateVisualContentCallback}
|
|
|
3
3
|
import {parseStyleValue, parseColor} from './style-parser';
|
|
4
4
|
|
|
5
5
|
// CSObject is an alias for VisualElement - they represent the same C# objects
|
|
6
|
-
type CSObject = VisualElement;
|
|
6
|
+
type CSObject = VisualElement & { pickingMode?: number };
|
|
7
7
|
|
|
8
8
|
// Global declarations for QuickJS environment
|
|
9
9
|
declare function setTimeout(callback: () => void, ms?: number): number;
|
|
@@ -54,6 +54,7 @@ declare const CS: {
|
|
|
54
54
|
AlternatingRowBackground: CSEnum;
|
|
55
55
|
CollectionVirtualizationMethod: CSEnum;
|
|
56
56
|
DisplayStyle: CSEnum;
|
|
57
|
+
PickingMode: CSEnum;
|
|
57
58
|
};
|
|
58
59
|
};
|
|
59
60
|
OneJS: {
|
|
@@ -70,6 +71,8 @@ declare const __eventAPI: {
|
|
|
70
71
|
addEventListener: (element: CSObject, eventType: string, callback: Function) => void;
|
|
71
72
|
removeEventListener: (element: CSObject, eventType: string, callback: Function) => void;
|
|
72
73
|
removeAllEventListeners: (element: CSObject) => void;
|
|
74
|
+
setParent: (childHandle: number, parentHandle: number) => void;
|
|
75
|
+
removeParent: (childHandle: number) => void;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
interface CSStyle {
|
|
@@ -416,6 +419,22 @@ function updateClassNames(element: CSObject, oldClassName: string | undefined, n
|
|
|
416
419
|
}
|
|
417
420
|
}
|
|
418
421
|
|
|
422
|
+
// Track parent-child relationships for event bubbling
|
|
423
|
+
function trackParent(child: CSObject, parent: CSObject) {
|
|
424
|
+
const childHandle = (child as unknown as { __csHandle: number }).__csHandle;
|
|
425
|
+
const parentHandle = (parent as unknown as { __csHandle: number }).__csHandle;
|
|
426
|
+
if (childHandle > 0 && parentHandle > 0) {
|
|
427
|
+
__eventAPI.setParent(childHandle, parentHandle);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function untrackParent(child: CSObject) {
|
|
432
|
+
const childHandle = (child as unknown as { __csHandle: number }).__csHandle;
|
|
433
|
+
if (childHandle > 0) {
|
|
434
|
+
__eventAPI.removeParent(childHandle);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
419
438
|
// Apply event handlers
|
|
420
439
|
function applyEvents(instance: Instance, props: BaseProps) {
|
|
421
440
|
for (const [propName, eventType] of Object.entries(EVENT_PROPS)) {
|
|
@@ -778,6 +797,11 @@ function createInstance(type: string, props: BaseProps): Instance {
|
|
|
778
797
|
applyVisualContentCallback(instance, props);
|
|
779
798
|
applyComponentProps(element, type, props as Record<string, unknown>);
|
|
780
799
|
|
|
800
|
+
// Apply pickingMode
|
|
801
|
+
if (props.pickingMode !== undefined) {
|
|
802
|
+
element.pickingMode = CS.UnityEngine.UIElements.PickingMode[props.pickingMode];
|
|
803
|
+
}
|
|
804
|
+
|
|
781
805
|
return instance;
|
|
782
806
|
}
|
|
783
807
|
|
|
@@ -806,6 +830,16 @@ function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseP
|
|
|
806
830
|
// Update component-specific props
|
|
807
831
|
applyComponentProps(element, instance.type, newProps as Record<string, unknown>);
|
|
808
832
|
|
|
833
|
+
// Update pickingMode
|
|
834
|
+
if (oldProps.pickingMode !== newProps.pickingMode) {
|
|
835
|
+
if (newProps.pickingMode !== undefined) {
|
|
836
|
+
element.pickingMode = CS.UnityEngine.UIElements.PickingMode[newProps.pickingMode];
|
|
837
|
+
} else {
|
|
838
|
+
// Reset to default (Position)
|
|
839
|
+
element.pickingMode = CS.UnityEngine.UIElements.PickingMode.Position;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
809
843
|
instance.props = newProps;
|
|
810
844
|
}
|
|
811
845
|
|
|
@@ -862,31 +896,31 @@ export const hostConfig = {
|
|
|
862
896
|
if (shouldMergeText(parentInstance, child)) {
|
|
863
897
|
appendMergedTextChild(parentInstance, child);
|
|
864
898
|
} else {
|
|
865
|
-
// Non-text child - unmerge any previously merged text first
|
|
866
899
|
handleNonTextChild(parentInstance);
|
|
867
900
|
parentInstance.element.Add(child.element);
|
|
868
901
|
}
|
|
902
|
+
trackParent(child.element, parentInstance.element);
|
|
869
903
|
},
|
|
870
904
|
|
|
871
905
|
appendChild(parentInstance: Instance, child: Instance) {
|
|
872
906
|
if (shouldMergeText(parentInstance, child)) {
|
|
873
907
|
appendMergedTextChild(parentInstance, child);
|
|
874
908
|
} else {
|
|
875
|
-
// Non-text child - unmerge any previously merged text first
|
|
876
909
|
handleNonTextChild(parentInstance);
|
|
877
910
|
parentInstance.element.Add(child.element);
|
|
878
911
|
}
|
|
912
|
+
trackParent(child.element, parentInstance.element);
|
|
879
913
|
},
|
|
880
914
|
|
|
881
915
|
appendChildToContainer(container: Container, child: Instance) {
|
|
882
916
|
container.Add(child.element);
|
|
917
|
+
// Container is the root - no parent to track
|
|
883
918
|
},
|
|
884
919
|
|
|
885
920
|
insertBefore(parentInstance: Instance, child: Instance, beforeChild: Instance) {
|
|
886
921
|
if (shouldMergeText(parentInstance, child)) {
|
|
887
922
|
insertMergedTextChild(parentInstance, child, beforeChild);
|
|
888
923
|
} else {
|
|
889
|
-
// Non-text child - unmerge any previously merged text first
|
|
890
924
|
handleNonTextChild(parentInstance);
|
|
891
925
|
const index = parentInstance.element.IndexOf(beforeChild.element);
|
|
892
926
|
if (index >= 0) {
|
|
@@ -895,6 +929,7 @@ export const hostConfig = {
|
|
|
895
929
|
parentInstance.element.Add(child.element);
|
|
896
930
|
}
|
|
897
931
|
}
|
|
932
|
+
trackParent(child.element, parentInstance.element);
|
|
898
933
|
},
|
|
899
934
|
|
|
900
935
|
insertInContainerBefore(container: Container, child: Instance, beforeChild: Instance) {
|
|
@@ -904,21 +939,23 @@ export const hostConfig = {
|
|
|
904
939
|
} else {
|
|
905
940
|
container.Add(child.element);
|
|
906
941
|
}
|
|
942
|
+
// Container is the root - no parent to track
|
|
907
943
|
},
|
|
908
944
|
|
|
909
945
|
removeChild(parentInstance: Instance, child: Instance) {
|
|
910
946
|
if (child.mergedInto === parentInstance) {
|
|
911
|
-
// Child was merged into parent's text - remove from merged children
|
|
912
947
|
removeMergedTextChild(parentInstance, child);
|
|
913
948
|
} else {
|
|
914
949
|
__eventAPI.removeAllEventListeners(child.element);
|
|
915
950
|
parentInstance.element.Remove(child.element);
|
|
916
951
|
}
|
|
952
|
+
untrackParent(child.element);
|
|
917
953
|
},
|
|
918
954
|
|
|
919
955
|
removeChildFromContainer(container: Container, child: Instance) {
|
|
920
956
|
__eventAPI.removeAllEventListeners(child.element);
|
|
921
957
|
container.Remove(child.element);
|
|
958
|
+
untrackParent(child.element);
|
|
922
959
|
},
|
|
923
960
|
|
|
924
961
|
prepareUpdate(_instance: Instance, _type: string, oldProps: BaseProps, newProps: BaseProps) {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -331,6 +331,14 @@ export interface BaseProps {
|
|
|
331
331
|
onTransitionEnd?: TransitionEventHandler;
|
|
332
332
|
onTransitionCancel?: TransitionEventHandler;
|
|
333
333
|
|
|
334
|
+
// Picking mode - controls whether the element receives pointer events
|
|
335
|
+
/**
|
|
336
|
+
* Controls whether this element is pickable by pointer events.
|
|
337
|
+
* - "Position" (default): Element receives pointer events based on its rectangle
|
|
338
|
+
* - "Ignore": Element is transparent to pointer events (clicks pass through)
|
|
339
|
+
*/
|
|
340
|
+
pickingMode?: 'Position' | 'Ignore';
|
|
341
|
+
|
|
334
342
|
// Vector drawing
|
|
335
343
|
/**
|
|
336
344
|
* Callback for custom vector drawing via Unity's generateVisualContent.
|