onejs-react 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ React 19 reconciler for Unity's UI Toolkit.
10
10
  | `src/renderer.ts` | Entry point: `render(element, container)` |
11
11
  | `src/components.tsx` | Component wrappers: View, Text, Label, Button, TextField, etc. |
12
12
  | `src/screen.tsx` | Responsive design: ScreenProvider, useBreakpoint, useScreenSize, useResponsive |
13
- | `src/types.ts` | TypeScript type definitions |
13
+ | `src/types.ts` | TypeScript type definitions (includes Vector Drawing types) |
14
14
  | `src/index.ts` | Package exports |
15
15
 
16
16
  ## Components
@@ -48,6 +48,70 @@ function App() {
48
48
  render(<App />, __root);
49
49
  ```
50
50
 
51
+ ## Type Usage Guide
52
+
53
+ OneJS has multiple type sources. Here's when to use each:
54
+
55
+ ### React Components (Most Common)
56
+
57
+ Import types from `onejs-react` for refs and component props:
58
+
59
+ ```tsx
60
+ import { View, Button, VisualElement, ButtonElement } from "onejs-react"
61
+
62
+ function MyComponent() {
63
+ const viewRef = useRef<VisualElement>(null)
64
+ const buttonRef = useRef<ButtonElement>(null)
65
+
66
+ useEffect(() => {
67
+ buttonRef.current?.Focus()
68
+ }, [])
69
+
70
+ return (
71
+ <View ref={viewRef}>
72
+ <Button ref={buttonRef} text="Click me" />
73
+ </View>
74
+ )
75
+ }
76
+ ```
77
+
78
+ ### Imperative Element Creation
79
+
80
+ For creating elements outside React, use `unity-types`:
81
+
82
+ ```tsx
83
+ import { Button } from "UnityEngine.UIElements"
84
+
85
+ const btn = new Button()
86
+ btn.text = "Dynamic Button"
87
+ __root.Add(btn)
88
+ ```
89
+
90
+ ### render() Container
91
+
92
+ The `render()` function accepts any `RenderContainer`:
93
+
94
+ ```tsx
95
+ import { render, RenderContainer } from "onejs-react"
96
+
97
+ // __root is provided by the runtime
98
+ render(<App />, __root)
99
+ ```
100
+
101
+ ### Type Hierarchy
102
+
103
+ ```
104
+ RenderContainer (minimal: __csHandle, __csType)
105
+ └── VisualElement (full API: style, hierarchy, events)
106
+ ├── TextElement (+ text property)
107
+ │ ├── LabelElement
108
+ │ └── ButtonElement
109
+ ├── TextFieldElement (+ value, isPasswordField, etc.)
110
+ ├── ToggleElement (+ value: boolean)
111
+ ├── SliderElement (+ value, lowValue, highValue)
112
+ └── ScrollViewElement (+ scrollOffset, ScrollTo)
113
+ ```
114
+
51
115
  ## Key Concepts
52
116
 
53
117
  - **Element types**: Use `ojs-` prefix internally (e.g., `ojs-view`, `ojs-button`) to avoid conflicts with HTML types
@@ -77,6 +141,123 @@ Test suite uses Vitest with mocked Unity CS globals. Tests are in `src/__tests__
77
141
  | `mocks.ts` | Mock implementations of Unity UI Toolkit classes |
78
142
  | `setup.ts` | Global test setup for CS, __eventAPI |
79
143
 
144
+ ## Vector Drawing
145
+
146
+ OneJS exposes Unity's `Painter2D` API for GPU-accelerated vector graphics. Any element can render custom vector content via `onGenerateVisualContent`.
147
+
148
+ ### Basic Usage
149
+
150
+ ```tsx
151
+ import { View, render } from "onejs-react"
152
+
153
+ function Circle() {
154
+ return (
155
+ <View
156
+ style={{ width: 200, height: 200, backgroundColor: "#333" }}
157
+ onGenerateVisualContent={(mgc) => {
158
+ const p = mgc.painter2D
159
+
160
+ // Draw a filled circle
161
+ p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1) // Red
162
+ p.BeginPath()
163
+ p.Arc(
164
+ new CS.UnityEngine.Vector2(100, 100), // center
165
+ 80, // radius
166
+ CS.UnityEngine.UIElements.Angle.Degrees(0),
167
+ CS.UnityEngine.UIElements.Angle.Degrees(360),
168
+ CS.UnityEngine.UIElements.ArcDirection.Clockwise
169
+ )
170
+ p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
171
+ }}
172
+ />
173
+ )
174
+ }
175
+ ```
176
+
177
+ ### Painter2D Methods
178
+
179
+ Path operations:
180
+ - `BeginPath()` - Start a new path
181
+ - `ClosePath()` - Close the current subpath
182
+ - `MoveTo(point)` - Move to point without drawing
183
+ - `LineTo(point)` - Draw line to point
184
+ - `Arc(center, radius, startAngle, endAngle, direction)` - Draw arc
185
+ - `ArcTo(p1, p2, radius)` - Draw arc tangent to two lines
186
+ - `BezierCurveTo(cp1, cp2, end)` - Cubic bezier curve
187
+ - `QuadraticCurveTo(cp, end)` - Quadratic bezier curve
188
+
189
+ Rendering:
190
+ - `Fill(fillRule)` - Fill the current path
191
+ - `Stroke()` - Stroke the current path
192
+
193
+ Properties:
194
+ - `fillColor` - Fill color (Unity Color)
195
+ - `strokeColor` - Stroke color (Unity Color)
196
+ - `lineWidth` - Stroke width in pixels
197
+ - `lineCap` - Line cap style (Butt, Round, Square)
198
+ - `lineJoin` - Line join style (Miter, Round, Bevel)
199
+
200
+ ### Triggering Repaints
201
+
202
+ Use `MarkDirtyRepaint()` to trigger a repaint when drawing state changes:
203
+
204
+ ```tsx
205
+ function AnimatedCircle() {
206
+ const ref = useRef<VisualElement>(null)
207
+ const [radius, setRadius] = useState(50)
208
+
209
+ useEffect(() => {
210
+ // Trigger repaint when radius changes
211
+ ref.current?.MarkDirtyRepaint()
212
+ }, [radius])
213
+
214
+ return (
215
+ <View
216
+ ref={ref}
217
+ style={{ width: 200, height: 200 }}
218
+ onGenerateVisualContent={(mgc) => {
219
+ const p = mgc.painter2D
220
+ p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
221
+ p.BeginPath()
222
+ p.Arc(
223
+ new CS.UnityEngine.Vector2(100, 100),
224
+ radius,
225
+ CS.UnityEngine.UIElements.Angle.Degrees(0),
226
+ CS.UnityEngine.UIElements.Angle.Degrees(360),
227
+ CS.UnityEngine.UIElements.ArcDirection.Clockwise
228
+ )
229
+ p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
230
+ }}
231
+ />
232
+ )
233
+ }
234
+ ```
235
+
236
+ ### Differences from HTML5 Canvas
237
+
238
+ | Feature | Unity Painter2D | HTML5 Canvas |
239
+ |---------|-----------------|--------------|
240
+ | Transforms | Manual point calculation | Built-in translate/rotate/scale |
241
+ | Gradients | Limited (strokeGradient) | Full linear/radial/conic |
242
+ | State Stack | Not built-in | save()/restore() |
243
+ | Text | Via MeshGenerationContext.DrawText() | fillText/strokeText |
244
+ | Shadows | Not available | shadowBlur, shadowColor |
245
+ | Clipping | Via nested VisualElements | clip() path-based |
246
+
247
+ ### Types
248
+
249
+ The following types are re-exported from `unity-types`:
250
+
251
+ ```typescript
252
+ type Vector2 = CS.UnityEngine.Vector2
253
+ type Color = CS.UnityEngine.Color
254
+ type Angle = CS.UnityEngine.UIElements.Angle
255
+ type ArcDirection = CS.UnityEngine.UIElements.ArcDirection
256
+ type Painter2D = CS.UnityEngine.UIElements.Painter2D
257
+ type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext
258
+ type GenerateVisualContentCallback = (context: MeshGenerationContext) => void
259
+ ```
260
+
80
261
  ## Dependencies
81
262
 
82
263
  - `react-reconciler@0.31.x` (React 19 compatible)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "React 19 renderer for OneJS (Unity UI Toolkit)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -38,6 +38,7 @@
38
38
  "@types/react-reconciler": "^0.28.9",
39
39
  "react": "^19.0.0",
40
40
  "typescript": "^5.7.0",
41
+ "unity-types": "file:../unity-types",
41
42
  "vitest": "^2.1.0"
42
43
  },
43
44
  "license": "MIT"
@@ -1,6 +1,6 @@
1
1
  import type {HostConfig} from 'react-reconciler';
2
- import type {BaseProps, ViewStyle, VisualElement} from './types';
3
- import {parseStyleValue} from './style-parser';
2
+ import type {BaseProps, ViewStyle, VisualElement, GenerateVisualContentCallback} from './types';
3
+ import {parseStyleValue, parseColor} from './style-parser';
4
4
 
5
5
  // CSObject is an alias for VisualElement - they represent the same C# objects
6
6
  type CSObject = VisualElement;
@@ -53,6 +53,7 @@ declare const CS: {
53
53
  ListViewReorderMode: CSEnum;
54
54
  AlternatingRowBackground: CSEnum;
55
55
  CollectionVirtualizationMethod: CSEnum;
56
+ DisplayStyle: CSEnum;
56
57
  };
57
58
  };
58
59
  OneJS: {
@@ -143,6 +144,8 @@ export interface Instance {
143
144
  mergedInto?: Instance;
144
145
  // Set to true when a non-text child is added, disabling further text merging
145
146
  hasMixedContent?: boolean;
147
+ // For vector drawing: track the current generateVisualContent callback
148
+ visualContentCallback?: GenerateVisualContentCallback;
146
149
  }
147
150
 
148
151
  export type TextInstance = Instance; // For Label elements with text content
@@ -432,6 +435,38 @@ function applyEvents(instance: Instance, props: BaseProps) {
432
435
  }
433
436
  }
434
437
 
438
+ /**
439
+ * Apply generateVisualContent callback for vector drawing.
440
+ * Uses Unity's generateVisualContent delegate on VisualElement.
441
+ *
442
+ * This follows the same pattern as ListView's makeItem/bindItem callbacks -
443
+ * we assign JS functions directly to C# delegate properties via the interop layer.
444
+ */
445
+ function applyVisualContentCallback(instance: Instance, props: BaseProps) {
446
+ const callback = props.onGenerateVisualContent;
447
+ const existingCallback = instance.visualContentCallback;
448
+
449
+ if (callback !== existingCallback) {
450
+ const element = instance.element as unknown as { generateVisualContent: GenerateVisualContentCallback | null };
451
+
452
+ // Remove old callback if exists
453
+ if (existingCallback) {
454
+ // Clear the delegate via C# interop
455
+ element.generateVisualContent = null;
456
+ }
457
+
458
+ // Add new callback if provided
459
+ if (callback) {
460
+ // Assign callback to generateVisualContent property
461
+ // The C# interop layer handles the delegate conversion
462
+ element.generateVisualContent = callback;
463
+ instance.visualContentCallback = callback;
464
+ } else {
465
+ instance.visualContentCallback = undefined;
466
+ }
467
+ }
468
+ }
469
+
435
470
  // MARK: Text Merging
436
471
  // Rebuild concatenated text from merged text children
437
472
  function rebuildMergedText(instance: Instance) {
@@ -541,6 +576,111 @@ function setValueProp<T>(target: T, key: keyof T, props: Record<string, unknown>
541
576
  }
542
577
  }
543
578
 
579
+ // Apply TextField-specific properties
580
+ function applyTextFieldProps(element: CSObject, props: Record<string, unknown>) {
581
+ // Map readOnly prop to isReadOnly property
582
+ if (props.readOnly !== undefined) {
583
+ (element as { isReadOnly: boolean }).isReadOnly = props.readOnly as boolean;
584
+ }
585
+ if (props.multiline !== undefined) {
586
+ (element as { multiline: boolean }).multiline = props.multiline as boolean;
587
+ }
588
+ if (props.maxLength !== undefined) {
589
+ (element as { maxLength: number }).maxLength = props.maxLength as number;
590
+ }
591
+ if (props.isPasswordField !== undefined) {
592
+ (element as { isPasswordField: boolean }).isPasswordField = props.isPasswordField as boolean;
593
+ }
594
+ if (props.maskChar !== undefined) {
595
+ (element as { maskChar: string }).maskChar = (props.maskChar as string).charAt(0);
596
+ }
597
+ if (props.isDelayed !== undefined) {
598
+ (element as { isDelayed: boolean }).isDelayed = props.isDelayed as boolean;
599
+ }
600
+ if (props.selectAllOnFocus !== undefined) {
601
+ (element as { selectAllOnFocus: boolean }).selectAllOnFocus = props.selectAllOnFocus as boolean;
602
+ }
603
+ if (props.selectAllOnMouseUp !== undefined) {
604
+ (element as { selectAllOnMouseUp: boolean }).selectAllOnMouseUp = props.selectAllOnMouseUp as boolean;
605
+ }
606
+ if (props.hideMobileInput !== undefined) {
607
+ (element as { hideMobileInput: boolean }).hideMobileInput = props.hideMobileInput as boolean;
608
+ }
609
+ if (props.autoCorrection !== undefined) {
610
+ (element as { autoCorrection: boolean }).autoCorrection = props.autoCorrection as boolean;
611
+ }
612
+ // Note: placeholder is handled differently in Unity - it's set via the textEdition interface
613
+ // For now we skip it as it requires more complex handling
614
+ }
615
+
616
+ // Apply Slider-specific properties
617
+ function applySliderProps(element: CSObject, props: Record<string, unknown>) {
618
+ if (props.lowValue !== undefined) {
619
+ (element as { lowValue: number }).lowValue = props.lowValue as number;
620
+ }
621
+ if (props.highValue !== undefined) {
622
+ (element as { highValue: number }).highValue = props.highValue as number;
623
+ }
624
+ if (props.showInputField !== undefined) {
625
+ (element as { showInputField: boolean }).showInputField = props.showInputField as boolean;
626
+ }
627
+ if (props.inverted !== undefined) {
628
+ (element as { inverted: boolean }).inverted = props.inverted as boolean;
629
+ }
630
+ if (props.pageSize !== undefined) {
631
+ (element as { pageSize: number }).pageSize = props.pageSize as number;
632
+ }
633
+ if (props.fill !== undefined) {
634
+ (element as { fill: boolean }).fill = props.fill as boolean;
635
+ }
636
+ if (props.direction !== undefined) {
637
+ const UIE = CS.UnityEngine.UIElements;
638
+ (element as { direction: unknown }).direction = UIE.SliderDirection[props.direction as string];
639
+ }
640
+ }
641
+
642
+ // Apply Toggle-specific properties
643
+ function applyToggleProps(element: CSObject, props: Record<string, unknown>) {
644
+ if (props.text !== undefined) {
645
+ (element as { text: string }).text = props.text as string;
646
+ }
647
+ if (props.toggleOnLabelClick !== undefined) {
648
+ (element as { toggleOnLabelClick: boolean }).toggleOnLabelClick = props.toggleOnLabelClick as boolean;
649
+ }
650
+ }
651
+
652
+ // Apply Image-specific properties
653
+ function applyImageProps(element: CSObject, props: Record<string, unknown>) {
654
+ if (props.image !== undefined) {
655
+ (element as { image: unknown }).image = props.image;
656
+ }
657
+ if (props.sprite !== undefined) {
658
+ (element as { sprite: unknown }).sprite = props.sprite;
659
+ }
660
+ if (props.vectorImage !== undefined) {
661
+ (element as { vectorImage: unknown }).vectorImage = props.vectorImage;
662
+ }
663
+ if (props.scaleMode !== undefined) {
664
+ const scaleMode = CS.UnityEngine.ScaleMode[props.scaleMode as string];
665
+ (element as { scaleMode: unknown }).scaleMode = scaleMode;
666
+ }
667
+ if (props.tintColor !== undefined) {
668
+ // Parse color string to Unity Color
669
+ const color = parseColor(props.tintColor as string);
670
+ if (color) {
671
+ (element as { tintColor: unknown }).tintColor = color;
672
+ }
673
+ }
674
+ if (props.sourceRect !== undefined) {
675
+ const rect = props.sourceRect as { x: number; y: number; width: number; height: number };
676
+ (element as { sourceRect: unknown }).sourceRect = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
677
+ }
678
+ if (props.uv !== undefined) {
679
+ const rect = props.uv as { x: number; y: number; width: number; height: number };
680
+ (element as { uv: unknown }).uv = new CS.UnityEngine.Rect(rect.x, rect.y, rect.width, rect.height);
681
+ }
682
+ }
683
+
544
684
  // Apply ScrollView-specific properties
545
685
  function applyScrollViewProps(element: CSScrollView, props: Record<string, unknown>) {
546
686
  const UIE = CS.UnityEngine.UIElements;
@@ -593,9 +733,23 @@ function applyListViewProps(element: CSListView, props: Record<string, unknown>)
593
733
 
594
734
  // Apply component-specific props based on element type
595
735
  function applyComponentProps(element: CSObject, type: string, props: Record<string, unknown>) {
736
+ // For Slider, apply range props (lowValue/highValue) BEFORE value
737
+ // Unity's Slider clamps value to [lowValue, highValue], so range must be set first
738
+ if (type === 'ojs-slider') {
739
+ applySliderProps(element, props);
740
+ applyCommonProps(element, props);
741
+ return;
742
+ }
743
+
596
744
  applyCommonProps(element, props);
597
745
 
598
- if (type === 'ojs-scrollview') {
746
+ if (type === 'ojs-textfield') {
747
+ applyTextFieldProps(element, props);
748
+ } else if (type === 'ojs-toggle') {
749
+ applyToggleProps(element, props);
750
+ } else if (type === 'ojs-image') {
751
+ applyImageProps(element, props);
752
+ } else if (type === 'ojs-scrollview') {
599
753
  applyScrollViewProps(element as CSScrollView, props);
600
754
  } else if (type === 'ojs-listview') {
601
755
  applyListViewProps(element as CSListView, props);
@@ -621,6 +775,7 @@ function createInstance(type: string, props: BaseProps): Instance {
621
775
 
622
776
  applyClassName(element, props.className);
623
777
  applyEvents(instance, props);
778
+ applyVisualContentCallback(instance, props);
624
779
  applyComponentProps(element, type, props as Record<string, unknown>);
625
780
 
626
781
  return instance;
@@ -645,6 +800,9 @@ function updateInstance(instance: Instance, oldProps: BaseProps, newProps: BaseP
645
800
  // Update events
646
801
  applyEvents(instance, newProps);
647
802
 
803
+ // Update vector drawing callback
804
+ applyVisualContentCallback(instance, newProps);
805
+
648
806
  // Update component-specific props
649
807
  applyComponentProps(element, instance.type, newProps as Record<string, unknown>);
650
808
 
@@ -887,16 +1045,16 @@ export const hostConfig = {
887
1045
 
888
1046
  // Visibility support
889
1047
  hideInstance(instance: Instance) {
890
- instance.element.style.display = 'none';
1048
+ instance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.None;
891
1049
  },
892
1050
  hideTextInstance(textInstance: TextInstance) {
893
- textInstance.element.style.display = 'none';
1051
+ textInstance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.None;
894
1052
  },
895
1053
  unhideInstance(instance: Instance, _props: BaseProps) {
896
- instance.element.style.display = '';
1054
+ instance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.Flex;
897
1055
  },
898
1056
  unhideTextInstance(textInstance: TextInstance, _text: string) {
899
- textInstance.element.style.display = '';
1057
+ textInstance.element.style.display = CS.UnityEngine.UIElements.DisplayStyle.Flex;
900
1058
  },
901
1059
 
902
1060
  // Text content
package/src/index.ts CHANGED
@@ -35,6 +35,9 @@ export type {
35
35
  BreakpointName,
36
36
  } from './screen';
37
37
 
38
+ // Vector Drawing
39
+ export { Transform2D, useVectorContent } from './vector';
40
+
38
41
  // Types
39
42
  export type {
40
43
  ViewStyle,
@@ -84,4 +87,12 @@ export type {
84
87
  SliderElement,
85
88
  ScrollViewElement,
86
89
  ImageElement,
90
+ // Vector drawing types
91
+ Vector2,
92
+ Color,
93
+ Angle,
94
+ ArcDirection,
95
+ Painter2D,
96
+ MeshGenerationContext,
97
+ GenerateVisualContentCallback,
87
98
  } from './types';
@@ -13,6 +13,17 @@ declare const CS: {
13
13
  Length: new (value: number, unit?: number) => CSLength;
14
14
  LengthUnit: { Pixel: number; Percent: number };
15
15
  StyleKeyword: { Auto: number; None: number; Initial: number };
16
+ // Enums for style properties
17
+ FlexDirection: Record<string, number>;
18
+ Wrap: Record<string, number>;
19
+ Align: Record<string, number>;
20
+ Justify: Record<string, number>;
21
+ Position: Record<string, number>;
22
+ Overflow: Record<string, number>;
23
+ DisplayStyle: Record<string, number>;
24
+ Visibility: Record<string, number>;
25
+ WhiteSpace: Record<string, number>;
26
+ TextAnchor: Record<string, number>;
16
27
  };
17
28
  };
18
29
  };
@@ -80,12 +91,119 @@ const NUMBER_PROPERTIES = new Set([
80
91
  "flexGrow", "flexShrink", "opacity",
81
92
  ])
82
93
 
83
- // Enum properties - these get passed through as-is (C# handles conversion)
84
- const ENUM_PROPERTIES = new Set([
85
- "flexDirection", "flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
86
- "position", "overflow", "display", "visibility", "whiteSpace",
87
- "unityTextAlign", "fontStyle",
88
- ])
94
+ // Enum property mappings: React style value -> Unity enum value
95
+ // Keys are camelCase (React/CSS style), values map to Unity enum member names
96
+ const ENUM_MAPPINGS: Record<string, { enum: () => Record<string, number>, values: Record<string, string> }> = {
97
+ flexDirection: {
98
+ enum: () => CS.UnityEngine.UIElements.FlexDirection,
99
+ values: {
100
+ "row": "Row",
101
+ "row-reverse": "RowReverse",
102
+ "column": "Column",
103
+ "column-reverse": "ColumnReverse",
104
+ }
105
+ },
106
+ flexWrap: {
107
+ enum: () => CS.UnityEngine.UIElements.Wrap,
108
+ values: {
109
+ "nowrap": "NoWrap",
110
+ "wrap": "Wrap",
111
+ "wrap-reverse": "WrapReverse",
112
+ }
113
+ },
114
+ alignItems: {
115
+ enum: () => CS.UnityEngine.UIElements.Align,
116
+ values: {
117
+ "auto": "Auto",
118
+ "flex-start": "FlexStart",
119
+ "flex-end": "FlexEnd",
120
+ "center": "Center",
121
+ "stretch": "Stretch",
122
+ }
123
+ },
124
+ alignSelf: {
125
+ enum: () => CS.UnityEngine.UIElements.Align,
126
+ values: {
127
+ "auto": "Auto",
128
+ "flex-start": "FlexStart",
129
+ "flex-end": "FlexEnd",
130
+ "center": "Center",
131
+ "stretch": "Stretch",
132
+ }
133
+ },
134
+ alignContent: {
135
+ enum: () => CS.UnityEngine.UIElements.Align,
136
+ values: {
137
+ "auto": "Auto",
138
+ "flex-start": "FlexStart",
139
+ "flex-end": "FlexEnd",
140
+ "center": "Center",
141
+ "stretch": "Stretch",
142
+ }
143
+ },
144
+ justifyContent: {
145
+ enum: () => CS.UnityEngine.UIElements.Justify,
146
+ values: {
147
+ "flex-start": "FlexStart",
148
+ "flex-end": "FlexEnd",
149
+ "center": "Center",
150
+ "space-between": "SpaceBetween",
151
+ "space-around": "SpaceAround",
152
+ }
153
+ },
154
+ position: {
155
+ enum: () => CS.UnityEngine.UIElements.Position,
156
+ values: {
157
+ "relative": "Relative",
158
+ "absolute": "Absolute",
159
+ }
160
+ },
161
+ overflow: {
162
+ enum: () => CS.UnityEngine.UIElements.Overflow,
163
+ values: {
164
+ "visible": "Visible",
165
+ "hidden": "Hidden",
166
+ }
167
+ },
168
+ display: {
169
+ enum: () => CS.UnityEngine.UIElements.DisplayStyle,
170
+ values: {
171
+ "flex": "Flex",
172
+ "none": "None",
173
+ }
174
+ },
175
+ visibility: {
176
+ enum: () => CS.UnityEngine.UIElements.Visibility,
177
+ values: {
178
+ "visible": "Visible",
179
+ "hidden": "Hidden",
180
+ }
181
+ },
182
+ whiteSpace: {
183
+ enum: () => CS.UnityEngine.UIElements.WhiteSpace,
184
+ values: {
185
+ "normal": "Normal",
186
+ "nowrap": "NoWrap",
187
+ }
188
+ },
189
+ }
190
+
191
+ /**
192
+ * Parse an enum style value
193
+ * @param key - Style property name
194
+ * @param value - String value from React style (e.g., "row", "flex-start")
195
+ * @returns Unity enum value or null if not found
196
+ */
197
+ function parseEnumValue(key: string, value: string): number | null {
198
+ const mapping = ENUM_MAPPINGS[key]
199
+ if (!mapping) return null
200
+
201
+ const unityEnumName = mapping.values[value]
202
+ if (!unityEnumName) return null
203
+
204
+ const enumType = mapping.enum()
205
+ return enumType[unityEnumName] ?? null
206
+ }
89
207
 
90
208
  /**
91
209
  * Parse a length value from various formats
@@ -236,9 +354,11 @@ export function parseStyleValue(key: string, value: unknown): unknown {
236
354
  return value
237
355
  }
238
356
 
239
- // Enum properties - pass through as-is (C# handles string -> enum)
240
- if (ENUM_PROPERTIES.has(key)) {
241
- return value
357
+ // Enum properties - convert string to Unity enum value
358
+ if (key in ENUM_MAPPINGS && typeof value === "string") {
359
+ const parsed = parseEnumValue(key, value)
360
+ if (parsed !== null) return parsed
361
+ // Fall through if parsing failed
242
362
  }
243
363
 
244
364
  // Unknown property - pass through unchanged
package/src/types.ts CHANGED
@@ -124,7 +124,7 @@ export interface ViewStyle {
124
124
  color?: StyleColor;
125
125
  /** Font size in pixels. Examples: 16, "16px" */
126
126
  fontSize?: StyleLength;
127
- fontStyle?: 'normal' | 'italic' | 'bold' | 'bold-and-italic';
127
+ /** Text alignment. Note: Use USS class or stylesheet for -unity-font-style (italic/bold) */
128
128
  unityTextAlign?: 'upper-left' | 'upper-center' | 'upper-right' | 'middle-left' | 'middle-center' | 'middle-right' | 'lower-left' | 'lower-center' | 'lower-right';
129
129
  whiteSpace?: 'normal' | 'nowrap';
130
130
  }
@@ -213,6 +213,55 @@ export type GeometryEventHandler = (event: GeometryEventData) => void;
213
213
  export type NavigationEventHandler = (event: NavigationEventData) => void;
214
214
  export type TransitionEventHandler = (event: TransitionEventData) => void;
215
215
 
216
+ // Vector Drawing Types - Re-export from unity-types (CS.* namespace)
217
+ // These types are provided by the unity-types package and represent Unity's actual API
218
+
219
+ /** Unity Vector2 - 2D point/vector. Use CS.UnityEngine.Vector2 at runtime. */
220
+ export type Vector2 = CS.UnityEngine.Vector2;
221
+
222
+ /** Unity Color - RGBA color. Use CS.UnityEngine.Color at runtime. */
223
+ export type Color = CS.UnityEngine.Color;
224
+
225
+ /** Unity Angle - Represents an angle with unit. Use CS.UnityEngine.UIElements.Angle at runtime. */
226
+ export type Angle = CS.UnityEngine.UIElements.Angle;
227
+
228
+ /** Unity ArcDirection - Direction for arc drawing. Use CS.UnityEngine.UIElements.ArcDirection at runtime. */
229
+ export type ArcDirection = CS.UnityEngine.UIElements.ArcDirection;
230
+
231
+ /** Unity Painter2D - Vector drawing API. Accessed via mgc.painter2D in generateVisualContent. */
232
+ export type Painter2D = CS.UnityEngine.UIElements.Painter2D;
233
+
234
+ /**
235
+ * Unity MeshGenerationContext - Provides rendering context within generateVisualContent callback.
236
+ *
237
+ * Access painter2D for vector drawing, or use DrawText/DrawVectorImage for other content.
238
+ *
239
+ * @example
240
+ * <View
241
+ * style={{ width: 200, height: 200 }}
242
+ * onGenerateVisualContent={(mgc) => {
243
+ * const p = mgc.painter2D
244
+ *
245
+ * p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
246
+ * p.BeginPath()
247
+ * p.Arc(
248
+ * new CS.UnityEngine.Vector2(100, 100),
249
+ * 80,
250
+ * CS.UnityEngine.UIElements.Angle.Degrees(0),
251
+ * CS.UnityEngine.UIElements.Angle.Degrees(360),
252
+ * CS.UnityEngine.UIElements.ArcDirection.Clockwise
253
+ * )
254
+ * p.Fill()
255
+ * }}
256
+ * />
257
+ */
258
+ export type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext;
259
+
260
+ /**
261
+ * Callback type for generateVisualContent
262
+ */
263
+ export type GenerateVisualContentCallback = (context: MeshGenerationContext) => void;
264
+
216
265
  // Base props for all components
217
266
  export interface BaseProps {
218
267
  key?: string | number;
@@ -281,6 +330,35 @@ export interface BaseProps {
281
330
  onTransitionStart?: TransitionEventHandler;
282
331
  onTransitionEnd?: TransitionEventHandler;
283
332
  onTransitionCancel?: TransitionEventHandler;
333
+
334
+ // Vector drawing
335
+ /**
336
+ * Callback for custom vector drawing via Unity's generateVisualContent.
337
+ * Called when the element needs to repaint its visual content.
338
+ *
339
+ * Use element.MarkDirtyRepaint() to trigger a repaint when your drawing state changes.
340
+ *
341
+ * @example
342
+ * <View
343
+ * style={{ width: 200, height: 200 }}
344
+ * onGenerateVisualContent={(mgc) => {
345
+ * const p = mgc.painter2D
346
+ * const Angle = CS.UnityEngine.UIElements.Angle
347
+ *
348
+ * p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
349
+ * p.BeginPath()
350
+ * p.Arc(
351
+ * new CS.UnityEngine.Vector2(100, 100),
352
+ * 80,
353
+ * Angle.Degrees(0),
354
+ * Angle.Degrees(360),
355
+ * CS.UnityEngine.UIElements.ArcDirection.Clockwise
356
+ * )
357
+ * p.Fill()
358
+ * }}
359
+ * />
360
+ */
361
+ onGenerateVisualContent?: GenerateVisualContentCallback;
284
362
  }
285
363
 
286
364
  // Component-specific props
@@ -300,23 +378,39 @@ export interface ButtonProps extends BaseProps {
300
378
 
301
379
  export interface TextFieldProps extends BaseProps {
302
380
  value?: string;
381
+ label?: string;
303
382
  placeholder?: string;
304
383
  multiline?: boolean;
305
384
  readOnly?: boolean;
306
385
  maxLength?: number;
386
+ isPasswordField?: boolean;
387
+ maskChar?: string;
388
+ isDelayed?: boolean;
389
+ selectAllOnFocus?: boolean;
390
+ selectAllOnMouseUp?: boolean;
391
+ hideMobileInput?: boolean;
392
+ autoCorrection?: boolean;
307
393
  onChange?: ChangeEventHandler<string>;
308
394
  }
309
395
 
310
396
  export interface ToggleProps extends BaseProps {
311
397
  value?: boolean;
312
398
  label?: string;
399
+ text?: string;
400
+ toggleOnLabelClick?: boolean;
313
401
  onChange?: ChangeEventHandler<boolean>;
314
402
  }
315
403
 
316
404
  export interface SliderProps extends BaseProps {
317
405
  value?: number;
406
+ label?: string;
318
407
  lowValue?: number;
319
408
  highValue?: number;
409
+ direction?: 'Horizontal' | 'Vertical';
410
+ pageSize?: number;
411
+ showInputField?: boolean;
412
+ inverted?: boolean;
413
+ fill?: boolean;
320
414
  onChange?: ChangeEventHandler<number>;
321
415
  }
322
416
 
@@ -344,8 +438,20 @@ export interface ScrollViewProps extends BaseProps {
344
438
  }
345
439
 
346
440
  export interface ImageProps extends BaseProps {
347
- src?: string;
348
- scaleMode?: 'stretch-to-fill' | 'scale-and-crop' | 'scale-to-fit';
441
+ /** Image source - can be a Texture2D, Sprite, or path string */
442
+ image?: object;
443
+ /** Sprite to display (alternative to image) */
444
+ sprite?: object;
445
+ /** Vector image to display */
446
+ vectorImage?: object;
447
+ /** How the image scales to fit the element */
448
+ scaleMode?: 'StretchToFill' | 'ScaleAndCrop' | 'ScaleToFit';
449
+ /** Tint color applied to the image */
450
+ tintColor?: string;
451
+ /** Source rectangle within the texture (normalized 0-1 coordinates) */
452
+ sourceRect?: { x: number; y: number; width: number; height: number };
453
+ /** UV coordinates for the image */
454
+ uv?: { x: number; y: number; width: number; height: number };
349
455
  }
350
456
 
351
457
  /**
@@ -400,6 +506,27 @@ export interface VisualElement extends RenderContainer {
400
506
 
401
507
  // Layout
402
508
  MarkDirtyRepaint: () => void;
509
+
510
+ // Vector drawing
511
+ /**
512
+ * Callback delegate for custom visual content generation.
513
+ * Can be used via ref for raw access to Unity's generateVisualContent.
514
+ *
515
+ * @example
516
+ * useEffect(() => {
517
+ * const ve = ref.current
518
+ * const draw = (mgc) => { ... }
519
+ * ve.generateVisualContent += draw
520
+ * return () => { ve.generateVisualContent -= draw }
521
+ * }, [])
522
+ */
523
+ generateVisualContent: {
524
+ (callback: GenerateVisualContentCallback): void;
525
+ } & {
526
+ // Delegate operators (C# interop)
527
+ '+='?: (callback: GenerateVisualContentCallback) => void;
528
+ '-='?: (callback: GenerateVisualContentCallback) => void;
529
+ };
403
530
  }
404
531
 
405
532
  // Specific element types for better ref typing
package/src/vector.ts ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Vector drawing utilities for OneJS.
3
+ *
4
+ * Provides Transform2D for applying 2D transformations to drawing coordinates,
5
+ * since Unity's Painter2D doesn't have built-in transform support.
6
+ *
7
+ * Also provides useVectorContent hook for automatic repaint on dependency changes.
8
+ */
9
+
10
+ import { useRef, useEffect, useCallback, type DependencyList, type RefObject } from 'react'
11
+ import type { Vector2, VisualElement, MeshGenerationContext, GenerateVisualContentCallback } from './types'
12
+
13
+ // Global declarations for Unity interop
14
+ declare const CS: {
15
+ UnityEngine: {
16
+ Vector2: new (x: number, y: number) => Vector2;
17
+ };
18
+ };
19
+
20
+ /**
21
+ * 2D transformation helper for vector drawing.
22
+ *
23
+ * Unity's Painter2D doesn't support transforms (translate, rotate, scale).
24
+ * This class provides client-side matrix math to transform coordinates
25
+ * before passing them to Painter2D methods.
26
+ *
27
+ * @example
28
+ * import { Transform2D } from "onejs-react"
29
+ *
30
+ * <View
31
+ * style={{ width: 200, height: 200 }}
32
+ * onGenerateVisualContent={(mgc) => {
33
+ * const p = mgc.painter2D
34
+ * const t = new Transform2D()
35
+ *
36
+ * // Center and rotate 45 degrees
37
+ * t.translate(100, 100)
38
+ * t.rotate(Math.PI / 4)
39
+ *
40
+ * // Draw a square using transformed coordinates
41
+ * p.BeginPath()
42
+ * p.MoveTo(t.point(-40, -40))
43
+ * p.LineTo(t.point(40, -40))
44
+ * p.LineTo(t.point(40, 40))
45
+ * p.LineTo(t.point(-40, 40))
46
+ * p.ClosePath()
47
+ * p.Fill()
48
+ * }}
49
+ * />
50
+ */
51
+ export class Transform2D {
52
+ // Current transformation matrix (a, b, c, d, e, f)
53
+ // [ a c e ] [ x ] [ a*x + c*y + e ]
54
+ // [ b d f ] * [ y ] = [ b*x + d*y + f ]
55
+ // [ 0 0 1 ] [ 1 ] [ 1 ]
56
+ private _a: number = 1
57
+ private _b: number = 0
58
+ private _c: number = 0
59
+ private _d: number = 1
60
+ private _e: number = 0
61
+ private _f: number = 0
62
+
63
+ // State stack for save/restore
64
+ private _stack: Array<[number, number, number, number, number, number]> = []
65
+
66
+ /**
67
+ * Save the current transformation state to the stack.
68
+ * Use restore() to return to this state later.
69
+ */
70
+ save(): void {
71
+ this._stack.push([this._a, this._b, this._c, this._d, this._e, this._f])
72
+ }
73
+
74
+ /**
75
+ * Restore the most recently saved transformation state.
76
+ * If the stack is empty, resets to identity.
77
+ */
78
+ restore(): void {
79
+ const state = this._stack.pop()
80
+ if (state) {
81
+ [this._a, this._b, this._c, this._d, this._e, this._f] = state
82
+ } else {
83
+ this.reset()
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Reset the transformation to identity (no transformation).
89
+ */
90
+ reset(): void {
91
+ this._a = 1
92
+ this._b = 0
93
+ this._c = 0
94
+ this._d = 1
95
+ this._e = 0
96
+ this._f = 0
97
+ }
98
+
99
+ /**
100
+ * Apply a translation (move the origin).
101
+ * @param x - Horizontal translation
102
+ * @param y - Vertical translation
103
+ */
104
+ translate(x: number, y: number): void {
105
+ // new_e = a*x + c*y + e
106
+ // new_f = b*x + d*y + f
107
+ this._e += this._a * x + this._c * y
108
+ this._f += this._b * x + this._d * y
109
+ }
110
+
111
+ /**
112
+ * Apply a rotation around the current origin.
113
+ * @param angle - Rotation angle in radians (clockwise)
114
+ */
115
+ rotate(angle: number): void {
116
+ const cos = Math.cos(angle)
117
+ const sin = Math.sin(angle)
118
+
119
+ // Multiply current matrix by rotation matrix
120
+ const a = this._a * cos + this._c * sin
121
+ const b = this._b * cos + this._d * sin
122
+ const c = this._a * -sin + this._c * cos
123
+ const d = this._b * -sin + this._d * cos
124
+
125
+ this._a = a
126
+ this._b = b
127
+ this._c = c
128
+ this._d = d
129
+ }
130
+
131
+ /**
132
+ * Apply a scale transformation.
133
+ * @param x - Horizontal scale factor
134
+ * @param y - Vertical scale factor (defaults to x for uniform scale)
135
+ */
136
+ scale(x: number, y?: number): void {
137
+ const sy = y ?? x
138
+
139
+ this._a *= x
140
+ this._b *= x
141
+ this._c *= sy
142
+ this._d *= sy
143
+ }
144
+
145
+ /**
146
+ * Transform a point using the current transformation matrix.
147
+ * @param x - X coordinate in local space
148
+ * @param y - Y coordinate in local space
149
+ * @returns Transformed point as Unity Vector2
150
+ */
151
+ point(x: number, y: number): Vector2 {
152
+ const tx = this._a * x + this._c * y + this._e
153
+ const ty = this._b * x + this._d * y + this._f
154
+ return new CS.UnityEngine.Vector2(tx, ty)
155
+ }
156
+
157
+ /**
158
+ * Transform multiple points at once.
159
+ * @param coords - Array of [x, y] coordinate pairs
160
+ * @returns Array of transformed Vector2 points
161
+ *
162
+ * @example
163
+ * const corners = t.points([-40, -40], [40, -40], [40, 40], [-40, 40])
164
+ * p.MoveTo(corners[0])
165
+ * for (let i = 1; i < corners.length; i++) p.LineTo(corners[i])
166
+ */
167
+ points(...coords: [number, number][]): Vector2[] {
168
+ return coords.map(([x, y]) => this.point(x, y))
169
+ }
170
+
171
+ /**
172
+ * Get the raw transformation values.
173
+ * Useful for debugging or advanced matrix operations.
174
+ *
175
+ * Returns [a, b, c, d, e, f] where:
176
+ * - a, d: scale
177
+ * - b, c: rotation/skew
178
+ * - e, f: translation
179
+ */
180
+ get values(): [number, number, number, number, number, number] {
181
+ return [this._a, this._b, this._c, this._d, this._e, this._f]
182
+ }
183
+
184
+ /**
185
+ * Set the transformation matrix directly.
186
+ * @param a - Horizontal scale (1 = no scale)
187
+ * @param b - Vertical skew
188
+ * @param c - Horizontal skew
189
+ * @param d - Vertical scale (1 = no scale)
190
+ * @param e - Horizontal translation
191
+ * @param f - Vertical translation
192
+ */
193
+ setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void {
194
+ this._a = a
195
+ this._b = b
196
+ this._c = c
197
+ this._d = d
198
+ this._e = e
199
+ this._f = f
200
+ }
201
+
202
+ /**
203
+ * Multiply the current matrix by another matrix.
204
+ * @param a - Horizontal scale
205
+ * @param b - Vertical skew
206
+ * @param c - Horizontal skew
207
+ * @param d - Vertical scale
208
+ * @param e - Horizontal translation
209
+ * @param f - Vertical translation
210
+ */
211
+ transform(a: number, b: number, c: number, d: number, e: number, f: number): void {
212
+ const a_ = this._a * a + this._c * b
213
+ const b_ = this._b * a + this._d * b
214
+ const c_ = this._a * c + this._c * d
215
+ const d_ = this._b * c + this._d * d
216
+ const e_ = this._a * e + this._c * f + this._e
217
+ const f_ = this._b * e + this._d * f + this._f
218
+
219
+ this._a = a_
220
+ this._b = b_
221
+ this._c = c_
222
+ this._d = d_
223
+ this._e = e_
224
+ this._f = f_
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Hook for vector drawing with automatic repaint on dependency changes.
230
+ *
231
+ * Returns a ref to attach to a VisualElement. When dependencies change,
232
+ * automatically calls MarkDirtyRepaint() to trigger a redraw.
233
+ *
234
+ * @param draw - Drawing callback that receives MeshGenerationContext
235
+ * @param deps - Dependency array (like useEffect) - repaint when these change
236
+ * @returns Ref to attach to the element
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * function AnimatedCircle() {
241
+ * const [radius, setRadius] = useState(50)
242
+ *
243
+ * const ref = useVectorContent((mgc) => {
244
+ * const p = mgc.painter2D
245
+ * const Angle = CS.UnityEngine.UIElements.Angle
246
+ *
247
+ * p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1)
248
+ * p.BeginPath()
249
+ * p.Arc(
250
+ * new CS.UnityEngine.Vector2(100, 100),
251
+ * radius,
252
+ * Angle.Degrees(0),
253
+ * Angle.Degrees(360),
254
+ * CS.UnityEngine.UIElements.ArcDirection.Clockwise
255
+ * )
256
+ * p.Fill()
257
+ * }, [radius]) // Auto-repaints when radius changes
258
+ *
259
+ * return <View ref={ref} style={{ width: 200, height: 200 }} />
260
+ * }
261
+ * ```
262
+ */
263
+ export function useVectorContent(
264
+ draw: GenerateVisualContentCallback,
265
+ deps: DependencyList = []
266
+ ): RefObject<VisualElement | null> {
267
+ const ref = useRef<VisualElement | null>(null)
268
+ const drawRef = useRef(draw)
269
+
270
+ // Keep drawRef current
271
+ drawRef.current = draw
272
+
273
+ // Register callback and handle updates
274
+ useEffect(() => {
275
+ const element = ref.current
276
+ if (!element) return
277
+
278
+ // Create a stable wrapper that always calls the latest draw function
279
+ const callback: GenerateVisualContentCallback = (mgc) => {
280
+ drawRef.current(mgc)
281
+ }
282
+
283
+ // Assign the callback to generateVisualContent
284
+ // Use unknown cast because VisualElement interface doesn't expose this property directly
285
+ const el = element as unknown as { generateVisualContent: GenerateVisualContentCallback | null }
286
+ el.generateVisualContent = callback
287
+
288
+ // Initial repaint to render content
289
+ element.MarkDirtyRepaint()
290
+
291
+ return () => {
292
+ // Clear callback on cleanup
293
+ el.generateVisualContent = null
294
+ }
295
+ }, []) // Only run once on mount
296
+
297
+ // Trigger repaint when dependencies change (but not on first render)
298
+ const isFirstRender = useRef(true)
299
+ useEffect(() => {
300
+ if (isFirstRender.current) {
301
+ isFirstRender.current = false
302
+ return
303
+ }
304
+
305
+ const element = ref.current
306
+ if (element) {
307
+ element.MarkDirtyRepaint()
308
+ }
309
+ }, deps)
310
+
311
+ return ref
312
+ }