onejs-react 0.1.17 → 0.1.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-react",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "React 19 renderer for OneJS (Unity UI Toolkit)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -36,6 +36,10 @@ const _imageCache = new Map<string, any>()
36
36
 
37
37
  function _resolveAssetPath(src: string): string {
38
38
  const Path = CS.System.IO.Path
39
+ // Absolute paths bypass asset resolution entirely
40
+ if (Path.IsPathRooted(src)) {
41
+ return src
42
+ }
39
43
  if (CS.UnityEngine.Application.isEditor) {
40
44
  const workingDir = typeof (globalThis as any).__workingDir === "string"
41
45
  ? (globalThis as any).__workingDir
@@ -316,6 +316,21 @@ function escapeClassName(name: string): string {
316
316
  return escaped;
317
317
  }
318
318
 
319
+ // Shallow equality check for plain objects (style, props).
320
+ // Returns true if both have identical own-property values (===).
321
+ function shallowEqual(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): boolean {
322
+ if (a === b) return true;
323
+ if (!a || !b) return false;
324
+ const keysA = Object.keys(a);
325
+ const keysB = Object.keys(b);
326
+ if (keysA.length !== keysB.length) return false;
327
+ for (let i = 0; i < keysA.length; i++) {
328
+ const k = keysA[i];
329
+ if (a[k] !== b[k]) return false;
330
+ }
331
+ return true;
332
+ }
333
+
319
334
  // Get all expanded property keys for a style object
320
335
  function getExpandedStyleKeys(style: ViewStyle | undefined): Set<string> {
321
336
  const keys = new Set<string>();
@@ -618,70 +633,68 @@ function removeMergedTextChild(parentInstance: Instance, child: Instance) {
618
633
 
619
634
  // MARK: Component-specific prop handlers
620
635
 
621
- // Apply common props (text, value, label)
622
- function applyCommonProps(element: CSObject, props: Record<string, unknown>) {
636
+ // Apply common props (text, value, label) - skip unchanged values
637
+ function applyCommonProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
623
638
  const el = element as any;
624
- if (props.text !== undefined) el.text = props.text as string;
625
- if (props.value !== undefined) el.value = props.value;
626
- if (props.label !== undefined) el.label = props.label as string;
639
+ if (props.text !== undefined && props.text !== oldProps?.text) el.text = props.text as string;
640
+ if (props.value !== undefined && props.value !== oldProps?.value) el.value = props.value;
641
+ if (props.label !== undefined && props.label !== oldProps?.label) el.label = props.label as string;
627
642
  }
628
643
 
629
- // Helper to set enum prop if defined
630
- function setEnumProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string, enumType: CSEnum) {
631
- if (props[propKey] !== undefined) {
644
+ // Helper to set enum prop if defined and changed
645
+ function setEnumProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string, enumType: CSEnum, oldProps?: Record<string, unknown>) {
646
+ if (props[propKey] !== undefined && props[propKey] !== oldProps?.[propKey]) {
632
647
  (target as Record<string, unknown>)[key as string] = enumType[props[propKey] as string];
633
648
  }
634
649
  }
635
650
 
636
- // Helper to set value prop if defined
637
- function setValueProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string) {
638
- if (props[propKey] !== undefined) {
651
+ // Helper to set value prop if defined and changed
652
+ function setValueProp<T>(target: T, key: keyof T, props: Record<string, unknown>, propKey: string, oldProps?: Record<string, unknown>) {
653
+ if (props[propKey] !== undefined && props[propKey] !== oldProps?.[propKey]) {
639
654
  (target as Record<string, unknown>)[key as string] = props[propKey];
640
655
  }
641
656
  }
642
657
 
643
- // Apply TextField-specific properties
644
- function applyTextFieldProps(element: CSObject, props: Record<string, unknown>) {
658
+ // Apply TextField-specific properties - skip unchanged values
659
+ function applyTextFieldProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
645
660
  const el = element as any;
646
- if (props.readOnly !== undefined) el.isReadOnly = props.readOnly;
647
- if (props.multiline !== undefined) el.multiline = props.multiline;
648
- if (props.maxLength !== undefined) el.maxLength = props.maxLength;
649
- if (props.isPasswordField !== undefined) el.isPasswordField = props.isPasswordField;
650
- if (props.maskChar !== undefined) el.maskChar = (props.maskChar as string).charAt(0);
651
- if (props.isDelayed !== undefined) el.isDelayed = props.isDelayed;
652
- if (props.selectAllOnFocus !== undefined) el.selectAllOnFocus = props.selectAllOnFocus;
653
- if (props.selectAllOnMouseUp !== undefined) el.selectAllOnMouseUp = props.selectAllOnMouseUp;
654
- if (props.hideMobileInput !== undefined) el.hideMobileInput = props.hideMobileInput;
655
- if (props.autoCorrection !== undefined) el.autoCorrection = props.autoCorrection;
656
- // Note: placeholder is handled differently in Unity - it's set via the textEdition interface
657
- // For now we skip it as it requires more complex handling
661
+ if (props.readOnly !== undefined && props.readOnly !== oldProps?.readOnly) el.isReadOnly = props.readOnly;
662
+ if (props.multiline !== undefined && props.multiline !== oldProps?.multiline) el.multiline = props.multiline;
663
+ if (props.maxLength !== undefined && props.maxLength !== oldProps?.maxLength) el.maxLength = props.maxLength;
664
+ if (props.isPasswordField !== undefined && props.isPasswordField !== oldProps?.isPasswordField) el.isPasswordField = props.isPasswordField;
665
+ if (props.maskChar !== undefined && props.maskChar !== oldProps?.maskChar) el.maskChar = (props.maskChar as string).charAt(0);
666
+ if (props.isDelayed !== undefined && props.isDelayed !== oldProps?.isDelayed) el.isDelayed = props.isDelayed;
667
+ if (props.selectAllOnFocus !== undefined && props.selectAllOnFocus !== oldProps?.selectAllOnFocus) el.selectAllOnFocus = props.selectAllOnFocus;
668
+ if (props.selectAllOnMouseUp !== undefined && props.selectAllOnMouseUp !== oldProps?.selectAllOnMouseUp) el.selectAllOnMouseUp = props.selectAllOnMouseUp;
669
+ if (props.hideMobileInput !== undefined && props.hideMobileInput !== oldProps?.hideMobileInput) el.hideMobileInput = props.hideMobileInput;
670
+ if (props.autoCorrection !== undefined && props.autoCorrection !== oldProps?.autoCorrection) el.autoCorrection = props.autoCorrection;
658
671
  }
659
672
 
660
- // Apply Slider-specific properties
661
- function applySliderProps(element: CSObject, props: Record<string, unknown>) {
673
+ // Apply Slider-specific properties - skip unchanged values
674
+ function applySliderProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
662
675
  const el = element as any;
663
- if (props.lowValue !== undefined) el.lowValue = props.lowValue;
664
- if (props.highValue !== undefined) el.highValue = props.highValue;
665
- if (props.showInputField !== undefined) el.showInputField = props.showInputField;
666
- if (props.inverted !== undefined) el.inverted = props.inverted;
667
- if (props.pageSize !== undefined) el.pageSize = props.pageSize;
668
- if (props.fill !== undefined) el.fill = props.fill;
669
- if (props.direction !== undefined) {
676
+ if (props.lowValue !== undefined && props.lowValue !== oldProps?.lowValue) el.lowValue = props.lowValue;
677
+ if (props.highValue !== undefined && props.highValue !== oldProps?.highValue) el.highValue = props.highValue;
678
+ if (props.showInputField !== undefined && props.showInputField !== oldProps?.showInputField) el.showInputField = props.showInputField;
679
+ if (props.inverted !== undefined && props.inverted !== oldProps?.inverted) el.inverted = props.inverted;
680
+ if (props.pageSize !== undefined && props.pageSize !== oldProps?.pageSize) el.pageSize = props.pageSize;
681
+ if (props.fill !== undefined && props.fill !== oldProps?.fill) el.fill = props.fill;
682
+ if (props.direction !== undefined && props.direction !== oldProps?.direction) {
670
683
  el.direction = CS.UnityEngine.UIElements.SliderDirection[props.direction as string];
671
684
  }
672
685
  }
673
686
 
674
- // Apply Toggle-specific properties
675
- function applyToggleProps(element: CSObject, props: Record<string, unknown>) {
687
+ // Apply Toggle-specific properties - skip unchanged values
688
+ function applyToggleProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
676
689
  const el = element as any;
677
- if (props.text !== undefined) el.text = props.text;
678
- if (props.toggleOnLabelClick !== undefined) el.toggleOnLabelClick = props.toggleOnLabelClick;
690
+ if (props.text !== undefined && props.text !== oldProps?.text) el.text = props.text;
691
+ if (props.toggleOnLabelClick !== undefined && props.toggleOnLabelClick !== oldProps?.toggleOnLabelClick) el.toggleOnLabelClick = props.toggleOnLabelClick;
679
692
  }
680
693
 
681
- // Apply Image-specific properties
682
- function applyImageProps(element: CSObject, props: Record<string, unknown>) {
694
+ // Apply Image-specific properties - skip unchanged values
695
+ function applyImageProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
683
696
  const el = element as any;
684
- if (props.image !== undefined) {
697
+ if (props.image !== undefined && props.image !== oldProps?.image) {
685
698
  const img = props.image;
686
699
  if (img != null && (img as any).GetType?.().Name === "VectorImage") {
687
700
  el.image = null;
@@ -691,73 +704,73 @@ function applyImageProps(element: CSObject, props: Record<string, unknown>) {
691
704
  el.image = img;
692
705
  }
693
706
  }
694
- if (props.sprite !== undefined) el.sprite = props.sprite;
695
- if (props.vectorImage !== undefined) el.vectorImage = props.vectorImage;
696
- if (props.scaleMode !== undefined) {
707
+ if (props.sprite !== undefined && props.sprite !== oldProps?.sprite) el.sprite = props.sprite;
708
+ if (props.vectorImage !== undefined && props.vectorImage !== oldProps?.vectorImage) el.vectorImage = props.vectorImage;
709
+ if (props.scaleMode !== undefined && props.scaleMode !== oldProps?.scaleMode) {
697
710
  el.scaleMode = CS.UnityEngine.ScaleMode[props.scaleMode as string];
698
711
  }
699
- if (props.tintColor !== undefined) {
712
+ if (props.tintColor !== undefined && props.tintColor !== oldProps?.tintColor) {
700
713
  const color = parseColor(props.tintColor as string);
701
714
  if (color) el.tintColor = color;
702
715
  }
703
- if (props.sourceRect !== undefined) {
716
+ if (props.sourceRect !== undefined && props.sourceRect !== oldProps?.sourceRect) {
704
717
  const rect = props.sourceRect as { x: number; y: number; width: number; height: number };
705
718
  el.sourceRect = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
706
719
  }
707
- if (props.uv !== undefined) {
720
+ if (props.uv !== undefined && props.uv !== oldProps?.uv) {
708
721
  const rect = props.uv as { x: number; y: number; width: number; height: number };
709
722
  el.uv = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
710
723
  }
711
724
  }
712
725
 
713
- // Apply ScrollView-specific properties
714
- function applyScrollViewProps(element: CSScrollView, props: Record<string, unknown>) {
726
+ // Apply ScrollView-specific properties - skip unchanged values
727
+ function applyScrollViewProps(element: CSScrollView, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
715
728
  const UIE = CS.UnityEngine.UIElements;
716
- setEnumProp(element, 'mode', props, 'mode', UIE.ScrollViewMode);
717
- setEnumProp(element, 'horizontalScrollerVisibility', props, 'horizontalScrollerVisibility', UIE.ScrollerVisibility);
718
- setEnumProp(element, 'verticalScrollerVisibility', props, 'verticalScrollerVisibility', UIE.ScrollerVisibility);
719
- setEnumProp(element, 'touchScrollBehavior', props, 'touchScrollBehavior', UIE.TouchScrollBehavior);
720
- setEnumProp(element, 'nestedInteractionKind', props, 'nestedInteractionKind', UIE.NestedInteractionKind);
721
- setValueProp(element, 'elasticity', props, 'elasticity');
722
- setValueProp(element, 'elasticAnimationIntervalMs', props, 'elasticAnimationIntervalMs');
723
- setValueProp(element, 'scrollDecelerationRate', props, 'scrollDecelerationRate');
724
- setValueProp(element, 'mouseWheelScrollSize', props, 'mouseWheelScrollSize');
725
- setValueProp(element, 'horizontalPageSize', props, 'horizontalPageSize');
726
- setValueProp(element, 'verticalPageSize', props, 'verticalPageSize');
729
+ setEnumProp(element, 'mode', props, 'mode', UIE.ScrollViewMode, oldProps);
730
+ setEnumProp(element, 'horizontalScrollerVisibility', props, 'horizontalScrollerVisibility', UIE.ScrollerVisibility, oldProps);
731
+ setEnumProp(element, 'verticalScrollerVisibility', props, 'verticalScrollerVisibility', UIE.ScrollerVisibility, oldProps);
732
+ setEnumProp(element, 'touchScrollBehavior', props, 'touchScrollBehavior', UIE.TouchScrollBehavior, oldProps);
733
+ setEnumProp(element, 'nestedInteractionKind', props, 'nestedInteractionKind', UIE.NestedInteractionKind, oldProps);
734
+ setValueProp(element, 'elasticity', props, 'elasticity', oldProps);
735
+ setValueProp(element, 'elasticAnimationIntervalMs', props, 'elasticAnimationIntervalMs', oldProps);
736
+ setValueProp(element, 'scrollDecelerationRate', props, 'scrollDecelerationRate', oldProps);
737
+ setValueProp(element, 'mouseWheelScrollSize', props, 'mouseWheelScrollSize', oldProps);
738
+ setValueProp(element, 'horizontalPageSize', props, 'horizontalPageSize', oldProps);
739
+ setValueProp(element, 'verticalPageSize', props, 'verticalPageSize', oldProps);
727
740
  }
728
741
 
729
- // Apply ListView-specific properties
730
- function applyListViewProps(element: CSListView, props: Record<string, unknown>) {
742
+ // Apply ListView-specific properties - skip unchanged values
743
+ function applyListViewProps(element: CSListView, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
731
744
  const UIE = CS.UnityEngine.UIElements;
732
745
 
733
746
  // Data binding callbacks
734
- setValueProp(element, 'itemsSource', props, 'itemsSource');
735
- setValueProp(element, 'makeItem', props, 'makeItem');
736
- setValueProp(element, 'bindItem', props, 'bindItem');
737
- setValueProp(element, 'unbindItem', props, 'unbindItem');
738
- setValueProp(element, 'destroyItem', props, 'destroyItem');
747
+ setValueProp(element, 'itemsSource', props, 'itemsSource', oldProps);
748
+ setValueProp(element, 'makeItem', props, 'makeItem', oldProps);
749
+ setValueProp(element, 'bindItem', props, 'bindItem', oldProps);
750
+ setValueProp(element, 'unbindItem', props, 'unbindItem', oldProps);
751
+ setValueProp(element, 'destroyItem', props, 'destroyItem', oldProps);
739
752
 
740
753
  // Virtualization
741
- setValueProp(element, 'fixedItemHeight', props, 'fixedItemHeight');
742
- setEnumProp(element, 'virtualizationMethod', props, 'virtualizationMethod', UIE.CollectionVirtualizationMethod);
754
+ setValueProp(element, 'fixedItemHeight', props, 'fixedItemHeight', oldProps);
755
+ setEnumProp(element, 'virtualizationMethod', props, 'virtualizationMethod', UIE.CollectionVirtualizationMethod, oldProps);
743
756
 
744
757
  // Selection
745
- setEnumProp(element, 'selectionType', props, 'selectionType', UIE.SelectionType);
746
- setValueProp(element, 'selectedIndex', props, 'selectedIndex');
747
- setValueProp(element, 'selectedIndices', props, 'selectedIndices');
758
+ setEnumProp(element, 'selectionType', props, 'selectionType', UIE.SelectionType, oldProps);
759
+ setValueProp(element, 'selectedIndex', props, 'selectedIndex', oldProps);
760
+ setValueProp(element, 'selectedIndices', props, 'selectedIndices', oldProps);
748
761
 
749
762
  // Reordering
750
- setValueProp(element, 'reorderable', props, 'reorderable');
751
- setEnumProp(element, 'reorderMode', props, 'reorderMode', UIE.ListViewReorderMode);
763
+ setValueProp(element, 'reorderable', props, 'reorderable', oldProps);
764
+ setEnumProp(element, 'reorderMode', props, 'reorderMode', UIE.ListViewReorderMode, oldProps);
752
765
 
753
766
  // Header/Footer
754
- setValueProp(element, 'showFoldoutHeader', props, 'showFoldoutHeader');
755
- setValueProp(element, 'headerTitle', props, 'headerTitle');
756
- setValueProp(element, 'showAddRemoveFooter', props, 'showAddRemoveFooter');
767
+ setValueProp(element, 'showFoldoutHeader', props, 'showFoldoutHeader', oldProps);
768
+ setValueProp(element, 'headerTitle', props, 'headerTitle', oldProps);
769
+ setValueProp(element, 'showAddRemoveFooter', props, 'showAddRemoveFooter', oldProps);
757
770
 
758
771
  // Appearance
759
- setValueProp(element, 'showBorder', props, 'showBorder');
760
- setEnumProp(element, 'showAlternatingRowBackgrounds', props, 'showAlternatingRowBackgrounds', UIE.AlternatingRowBackground);
772
+ setValueProp(element, 'showBorder', props, 'showBorder', oldProps);
773
+ setEnumProp(element, 'showAlternatingRowBackgrounds', props, 'showAlternatingRowBackgrounds', UIE.AlternatingRowBackground, oldProps);
761
774
  }
762
775
 
763
776
  // Props handled by the reconciler infrastructure - not forwarded to C# elements
@@ -767,46 +780,47 @@ const RESERVED_PROPS = new Set([
767
780
  ...Object.keys(EVENT_PROPS),
768
781
  ]);
769
782
 
770
- // Forward non-reserved props directly to C# element (for custom elements)
771
- function applyCustomProps(element: CSObject, props: Record<string, unknown>) {
783
+ // Forward non-reserved props directly to C# element (for custom elements) - skip unchanged
784
+ function applyCustomProps(element: CSObject, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
772
785
  for (const [key, value] of Object.entries(props)) {
773
786
  if (value === undefined || RESERVED_PROPS.has(key)) continue;
787
+ if (value === oldProps?.[key]) continue;
774
788
  (element as any)[key] = value;
775
789
  }
776
790
  }
777
791
 
778
792
  // Apply component-specific props based on element type
779
- function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>) {
793
+ function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>, oldProps?: Record<string, unknown>) {
780
794
  // Custom elements: forward all non-reserved props directly to C# element
781
795
  if (!BUILT_IN_TYPES.has(type)) {
782
- applyCustomProps(element, props);
796
+ applyCustomProps(element, props, oldProps);
783
797
  return;
784
798
  }
785
799
 
786
800
  // For Slider, apply range props (lowValue/highValue) BEFORE value
787
801
  // Unity's Slider clamps value to [lowValue, highValue], so range must be set first
788
802
  if (type === 'ojs-slider') {
789
- applySliderProps(element, props);
790
- applyCommonProps(element, props);
803
+ applySliderProps(element, props, oldProps);
804
+ applyCommonProps(element, props, oldProps);
791
805
  return;
792
806
  }
793
807
 
794
- applyCommonProps(element, props);
808
+ applyCommonProps(element, props, oldProps);
795
809
 
796
810
  if (type === 'ojs-textfield') {
797
- applyTextFieldProps(element, props);
811
+ applyTextFieldProps(element, props, oldProps);
798
812
  } else if (type === 'ojs-toggle') {
799
- applyToggleProps(element, props);
813
+ applyToggleProps(element, props, oldProps);
800
814
  } else if (type === 'ojs-image') {
801
- applyImageProps(element, props);
815
+ applyImageProps(element, props, oldProps);
802
816
  } else if (type === 'ojs-scrollview') {
803
- applyScrollViewProps(element as CSScrollView, props);
817
+ applyScrollViewProps(element as CSScrollView, props, oldProps);
804
818
  } else if (type === 'ojs-listview') {
805
- applyListViewProps(element as CSListView, props);
819
+ applyListViewProps(element as CSListView, props, oldProps);
806
820
  } else if (type === 'ojs-frostedglass') {
807
821
  const el = element as any;
808
- if (props.blurRadius !== undefined) el.BlurRadius = props.blurRadius;
809
- if (props.tintColor !== undefined) el.TintColor = props.tintColor;
822
+ if (props.blurRadius !== undefined && props.blurRadius !== oldProps?.blurRadius) el.BlurRadius = props.blurRadius;
823
+ if (props.tintColor !== undefined && props.tintColor !== oldProps?.tintColor) el.TintColor = props.tintColor;
810
824
  }
811
825
  }
812
826
 
@@ -844,8 +858,9 @@ function createInstance(type: string, props: BaseProps): Instance {
844
858
  function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseProps) {
845
859
  const element = instance.element;
846
860
 
847
- // Update style - clear removed properties, then apply new ones
848
- if (oldProps.style !== newProps.style) {
861
+ // Update style - skip if values are shallowly equal (inline style objects are
862
+ // new references each render but usually contain the same values)
863
+ if (oldProps.style !== newProps.style && !shallowEqual(oldProps.style as any, newProps.style as any)) {
849
864
  const newStyleKeys = getExpandedStyleKeys(newProps.style);
850
865
  clearRemovedStyles(element, instance.appliedStyleKeys, newStyleKeys);
851
866
  instance.appliedStyleKeys = applyStyle(element, newProps.style);
@@ -862,8 +877,8 @@ function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseP
862
877
  // Update vector drawing callback
863
878
  applyVisualContentCallback(instance, newProps);
864
879
 
865
- // Update component-specific props
866
- applyComponentProps(element, instance.type, newProps as Record<string, unknown>);
880
+ // Update component-specific props - pass oldProps to skip unchanged values
881
+ applyComponentProps(element, instance.type, newProps as Record<string, unknown>, oldProps as Record<string, unknown>);
867
882
 
868
883
  // Update pickingMode
869
884
  if (oldProps.pickingMode !== newProps.pickingMode) {