onejs-react 0.1.0 → 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 +265 -0
- package/package.json +3 -1
- package/src/__tests__/components.test.tsx +36 -0
- package/src/__tests__/host-config.test.ts +141 -99
- package/src/__tests__/mocks.ts +17 -1
- package/src/__tests__/setup.ts +23 -11
- package/src/components.tsx +77 -48
- package/src/error-boundary.tsx +175 -0
- package/src/host-config.ts +503 -162
- package/src/index.ts +46 -2
- package/src/renderer.ts +50 -23
- package/src/screen.tsx +1 -1
- package/src/style-parser.ts +171 -79
- package/src/types.ts +326 -8
- package/src/vector.ts +312 -0
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# onejs-react
|
|
2
|
+
|
|
3
|
+
React 19 reconciler for Unity's UI Toolkit.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `src/host-config.ts` | React reconciler implementation (createInstance, commitUpdate, etc.) |
|
|
10
|
+
| `src/renderer.ts` | Entry point: `render(element, container)` |
|
|
11
|
+
| `src/components.tsx` | Component wrappers: View, Text, Label, Button, TextField, etc. |
|
|
12
|
+
| `src/screen.tsx` | Responsive design: ScreenProvider, useBreakpoint, useScreenSize, useResponsive |
|
|
13
|
+
| `src/types.ts` | TypeScript type definitions (includes Vector Drawing types) |
|
|
14
|
+
| `src/index.ts` | Package exports |
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
| Component | UI Toolkit Element | Description |
|
|
19
|
+
|-----------|-------------------|-------------|
|
|
20
|
+
| `View` | VisualElement | Container element |
|
|
21
|
+
| `Text` | TextElement | Primary text display |
|
|
22
|
+
| `Label` | Label | Form labels, semantic labeling |
|
|
23
|
+
| `Button` | Button | Interactive button |
|
|
24
|
+
| `TextField` | TextField | Text input |
|
|
25
|
+
| `Toggle` | Toggle | Checkbox/toggle |
|
|
26
|
+
| `Slider` | Slider | Numeric slider |
|
|
27
|
+
| `ScrollView` | ScrollView | Scrollable container |
|
|
28
|
+
| `Image` | Image | Image display |
|
|
29
|
+
| `ListView` | ListView | Virtualized list |
|
|
30
|
+
|
|
31
|
+
**Raw text in JSX** (e.g., `<View>Hello</View>`) creates a `TextElement`, providing semantic distinction from explicit `<Label>` components.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { render, View, Text, Label, Button } from 'onejs-react';
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
return (
|
|
40
|
+
<View style={{ padding: 20 }}>
|
|
41
|
+
<Text text="Welcome!" style={{ fontSize: 24 }} />
|
|
42
|
+
<Button text="Click me" onClick={() => console.log('clicked')} />
|
|
43
|
+
<View>Raw text also works</View>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render(<App />, __root);
|
|
49
|
+
```
|
|
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
|
+
|
|
115
|
+
## Key Concepts
|
|
116
|
+
|
|
117
|
+
- **Element types**: Use `ojs-` prefix internally (e.g., `ojs-view`, `ojs-button`) to avoid conflicts with HTML types
|
|
118
|
+
- **Style shorthands**: `padding`/`margin` are expanded to individual properties (UI Toolkit requirement)
|
|
119
|
+
- **Style cleanup**: When props change, removed style properties are cleared (not just new ones applied)
|
|
120
|
+
- **className updates**: Selective add/remove of classes (not full clear + reapply)
|
|
121
|
+
- **Event handlers**: Registered via `__eventAPI` from QuickJSBootstrap.js
|
|
122
|
+
- **Instance structure**: `{ element, type, props, eventHandlers: Map, appliedStyleKeys: Set }`
|
|
123
|
+
|
|
124
|
+
## Build & Test
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run typecheck # TypeScript check (no build output - consumed directly by App)
|
|
128
|
+
npm test # Run test suite
|
|
129
|
+
npm run test:watch # Run tests in watch mode
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Testing
|
|
133
|
+
|
|
134
|
+
Test suite uses Vitest with mocked Unity CS globals. Tests are in `src/__tests__/`:
|
|
135
|
+
|
|
136
|
+
| File | Coverage |
|
|
137
|
+
|------|----------|
|
|
138
|
+
| `host-config.test.ts` | Instance creation, style/className management, events, children |
|
|
139
|
+
| `renderer.test.tsx` | Integration tests: render(), unmount(), React state, effects |
|
|
140
|
+
| `components.test.tsx` | Component wrappers, prop passing, event mapping |
|
|
141
|
+
| `mocks.ts` | Mock implementations of Unity UI Toolkit classes |
|
|
142
|
+
| `setup.ts` | Global test setup for CS, __eventAPI |
|
|
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
|
+
|
|
261
|
+
## Dependencies
|
|
262
|
+
|
|
263
|
+
- `react-reconciler@0.31.x` (React 19 compatible)
|
|
264
|
+
- `vitest` (dev) - Test runner
|
|
265
|
+
- Peer: `react@18.x || 19.x`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-react",
|
|
3
|
-
"version": "0.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",
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
"react-reconciler": "^0.31.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.0.3",
|
|
36
37
|
"@types/react": "^19.0.0",
|
|
37
38
|
"@types/react-reconciler": "^0.28.9",
|
|
38
39
|
"react": "^19.0.0",
|
|
39
40
|
"typescript": "^5.7.0",
|
|
41
|
+
"unity-types": "file:../unity-types",
|
|
40
42
|
"vitest": "^2.1.0"
|
|
41
43
|
},
|
|
42
44
|
"license": "MIT"
|
|
@@ -12,6 +12,7 @@ import React from 'react';
|
|
|
12
12
|
import { render } from '../renderer';
|
|
13
13
|
import {
|
|
14
14
|
View,
|
|
15
|
+
Text,
|
|
15
16
|
Label,
|
|
16
17
|
Button,
|
|
17
18
|
TextField,
|
|
@@ -108,6 +109,41 @@ describe('components', () => {
|
|
|
108
109
|
});
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
describe('Text', () => {
|
|
113
|
+
it('renders as TextElement', async () => {
|
|
114
|
+
const container = createMockContainer();
|
|
115
|
+
render(<Text text="Hello" />, container as any);
|
|
116
|
+
await flushMicrotasks();
|
|
117
|
+
|
|
118
|
+
expect(container.children[0].__csType).toBe('UnityEngine.UIElements.TextElement');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('sets text property', async () => {
|
|
122
|
+
const container = createMockContainer();
|
|
123
|
+
render(<Text text="Hello World" />, container as any);
|
|
124
|
+
await flushMicrotasks();
|
|
125
|
+
|
|
126
|
+
const el = container.children[0] as MockVisualElement;
|
|
127
|
+
expect(el.text).toBe('Hello World');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('applies styles', async () => {
|
|
131
|
+
const container = createMockContainer();
|
|
132
|
+
render(
|
|
133
|
+
<Text
|
|
134
|
+
text="Styled"
|
|
135
|
+
style={{ fontSize: 24, color: 'white' }}
|
|
136
|
+
/>,
|
|
137
|
+
container as any
|
|
138
|
+
);
|
|
139
|
+
await flushMicrotasks();
|
|
140
|
+
|
|
141
|
+
const el = container.children[0] as MockVisualElement;
|
|
142
|
+
expect(getStyleValue(el.style.fontSize)).toBe(24);
|
|
143
|
+
expect(el.style.color).toBeInstanceOf(MockColor);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
111
147
|
describe('Label', () => {
|
|
112
148
|
it('renders as Label element', async () => {
|
|
113
149
|
const container = createMockContainer();
|