Skip to content

Commit e88c2e0

Browse files
committed
refactor(nano): add component-specific render methods to NanoRenderer
- Refactor NanoRenderer interface to add dedicated methods for each component type - Update HtmlRenderer with all new interface methods (Form, TextArea, Select, Divider) - Refactor ComposeNanoRenderer to follow the same component-specific pattern - Add VSCode TypeScript NanoRenderer implementation with types and CSS This change ensures compile-time safety when adding new components - the compiler will enforce that all renderer implementations add corresponding methods, solving the issue where the 'else' branch would silently ignore unimplemented components. Closes #487
1 parent bbb930c commit e88c2e0

File tree

7 files changed

+1179
-102
lines changed

7 files changed

+1179
-102
lines changed

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/devins/ui/nano/ComposeNanoRenderer.kt

Lines changed: 159 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cc.unitmesh.devins.ui.nano
22

33
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
45
import androidx.compose.foundation.layout.*
56
import androidx.compose.foundation.shape.RoundedCornerShape
67
import androidx.compose.material3.*
@@ -20,53 +21,80 @@ import kotlinx.serialization.json.jsonPrimitive
2021
*
2122
* Renders NanoIR components to Compose UI.
2223
* Uses Material 3 components for consistent theming.
24+
*
25+
* This renderer follows the component-specific method pattern from NanoRenderer interface.
26+
* Each component type has its own Render* method, making it easy to identify
27+
* missing implementations when new components are added.
28+
*
29+
* @see cc.unitmesh.xuiper.render.NanoRenderer for the interface pattern
2330
*/
2431
object ComposeNanoRenderer {
2532

33+
// ============================================================================
34+
// Main Entry Point
35+
// ============================================================================
36+
37+
/**
38+
* Render a NanoIR node to Compose UI.
39+
* Dispatches to component-specific render methods.
40+
*/
2641
@Composable
2742
fun Render(ir: NanoIR, modifier: Modifier = Modifier) {
43+
RenderNode(ir, modifier)
44+
}
45+
46+
/**
47+
* Dispatch rendering based on component type.
48+
* Routes to the appropriate component-specific render method.
49+
*/
50+
@Composable
51+
fun RenderNode(ir: NanoIR, modifier: Modifier = Modifier) {
2852
when (ir.type) {
29-
"Component" -> RenderComponent(ir, modifier)
53+
// Layout
3054
"VStack" -> RenderVStack(ir, modifier)
3155
"HStack" -> RenderHStack(ir, modifier)
56+
// Container
3257
"Card" -> RenderCard(ir, modifier)
58+
"Form" -> RenderForm(ir, modifier)
59+
// Content
3360
"Text" -> RenderText(ir, modifier)
34-
"Button" -> RenderButton(ir, modifier)
3561
"Image" -> RenderImage(ir, modifier)
3662
"Badge" -> RenderBadge(ir, modifier)
63+
"Divider" -> RenderDivider(ir, modifier)
64+
// Input
65+
"Button" -> RenderButton(ir, modifier)
3766
"Input" -> RenderInput(ir, modifier)
3867
"Checkbox" -> RenderCheckbox(ir, modifier)
39-
"Divider" -> HorizontalDivider(modifier.padding(vertical = 8.dp))
68+
"TextArea" -> RenderTextArea(ir, modifier)
69+
"Select" -> RenderSelect(ir, modifier)
70+
// Control Flow
4071
"Conditional" -> RenderConditional(ir, modifier)
4172
"ForLoop" -> RenderForLoop(ir, modifier)
73+
// Meta
74+
"Component" -> RenderComponent(ir, modifier)
4275
else -> RenderUnknown(ir, modifier)
4376
}
4477
}
4578

46-
@Composable
47-
private fun RenderComponent(ir: NanoIR, modifier: Modifier) {
48-
Column(modifier = modifier) {
49-
ir.children?.forEach { child ->
50-
Render(child)
51-
}
52-
}
53-
}
79+
// ============================================================================
80+
// Layout Components
81+
// ============================================================================
5482

5583
@Composable
56-
private fun RenderVStack(ir: NanoIR, modifier: Modifier) {
84+
fun RenderVStack(ir: NanoIR, modifier: Modifier = Modifier) {
5785
val spacing = ir.props["spacing"]?.jsonPrimitive?.content?.toSpacing() ?: 8.dp
5886
Column(
5987
modifier = modifier,
6088
verticalArrangement = Arrangement.spacedBy(spacing)
6189
) {
6290
ir.children?.forEach { child ->
63-
Render(child)
91+
RenderNode(child)
6492
}
6593
}
6694
}
6795

6896
@Composable
69-
private fun RenderHStack(ir: NanoIR, modifier: Modifier) {
97+
fun RenderHStack(ir: NanoIR, modifier: Modifier = Modifier) {
7098
val spacing = ir.props["spacing"]?.jsonPrimitive?.content?.toSpacing() ?: 8.dp
7199
val align = ir.props["align"]?.jsonPrimitive?.content?.toVerticalAlignment() ?: Alignment.CenterVertically
72100
val justify = ir.props["justify"]?.jsonPrimitive?.content?.toHorizontalArrangement() ?: Arrangement.Start
@@ -77,13 +105,17 @@ object ComposeNanoRenderer {
77105
verticalAlignment = align
78106
) {
79107
ir.children?.forEach { child ->
80-
Render(child)
108+
RenderNode(child)
81109
}
82110
}
83111
}
84112

113+
// ============================================================================
114+
// Container Components
115+
// ============================================================================
116+
85117
@Composable
86-
private fun RenderCard(ir: NanoIR, modifier: Modifier) {
118+
fun RenderCard(ir: NanoIR, modifier: Modifier = Modifier) {
87119
val padding = ir.props["padding"]?.jsonPrimitive?.content?.toSpacing() ?: 16.dp
88120
val shadow = ir.props["shadow"]?.jsonPrimitive?.content?.toElevation() ?: 2.dp
89121

@@ -94,14 +126,30 @@ object ComposeNanoRenderer {
94126
) {
95127
Column(modifier = Modifier.padding(padding)) {
96128
ir.children?.forEach { child ->
97-
Render(child)
129+
RenderNode(child)
98130
}
99131
}
100132
}
101133
}
102134

103135
@Composable
104-
private fun RenderText(ir: NanoIR, modifier: Modifier) {
136+
fun RenderForm(ir: NanoIR, modifier: Modifier = Modifier) {
137+
Column(
138+
modifier = modifier.fillMaxWidth(),
139+
verticalArrangement = Arrangement.spacedBy(16.dp)
140+
) {
141+
ir.children?.forEach { child ->
142+
RenderNode(child)
143+
}
144+
}
145+
}
146+
147+
// ============================================================================
148+
// Content Components
149+
// ============================================================================
150+
151+
@Composable
152+
fun RenderText(ir: NanoIR, modifier: Modifier = Modifier) {
105153
val content = ir.props["content"]?.jsonPrimitive?.content ?: ""
106154
val style = ir.props["style"]?.jsonPrimitive?.content
107155

@@ -119,24 +167,7 @@ object ComposeNanoRenderer {
119167
}
120168

121169
@Composable
122-
private fun RenderButton(ir: NanoIR, modifier: Modifier) {
123-
val label = ir.props["label"]?.jsonPrimitive?.content ?: "Button"
124-
val intent = ir.props["intent"]?.jsonPrimitive?.content
125-
126-
val colors = when (intent) {
127-
"primary" -> ButtonDefaults.buttonColors()
128-
"secondary" -> ButtonDefaults.outlinedButtonColors()
129-
"danger" -> ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
130-
else -> ButtonDefaults.buttonColors()
131-
}
132-
133-
Button(onClick = { }, colors = colors, modifier = modifier) {
134-
Text(label)
135-
}
136-
}
137-
138-
@Composable
139-
private fun RenderImage(ir: NanoIR, modifier: Modifier) {
170+
fun RenderImage(ir: NanoIR, modifier: Modifier = Modifier) {
140171
val src = ir.props["src"]?.jsonPrimitive?.content ?: ""
141172
// Placeholder for image - in real app would load from URL
142173
Box(
@@ -152,7 +183,7 @@ object ComposeNanoRenderer {
152183
}
153184

154185
@Composable
155-
private fun RenderBadge(ir: NanoIR, modifier: Modifier) {
186+
fun RenderBadge(ir: NanoIR, modifier: Modifier = Modifier) {
156187
val text = ir.props["text"]?.jsonPrimitive?.content ?: ""
157188
val colorName = ir.props["color"]?.jsonPrimitive?.content
158189

@@ -185,19 +216,46 @@ object ComposeNanoRenderer {
185216
}
186217

187218
@Composable
188-
private fun RenderInput(ir: NanoIR, modifier: Modifier) {
219+
fun RenderDivider(ir: NanoIR, modifier: Modifier = Modifier) {
220+
HorizontalDivider(modifier.padding(vertical = 8.dp))
221+
}
222+
223+
// ============================================================================
224+
// Input Components
225+
// ============================================================================
226+
227+
@Composable
228+
fun RenderButton(ir: NanoIR, modifier: Modifier = Modifier) {
229+
val label = ir.props["label"]?.jsonPrimitive?.content ?: "Button"
230+
val intent = ir.props["intent"]?.jsonPrimitive?.content
231+
232+
val colors = when (intent) {
233+
"primary" -> ButtonDefaults.buttonColors()
234+
"secondary" -> ButtonDefaults.outlinedButtonColors()
235+
"danger" -> ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
236+
else -> ButtonDefaults.buttonColors()
237+
}
238+
239+
Button(onClick = { }, colors = colors, modifier = modifier) {
240+
Text(label)
241+
}
242+
}
243+
244+
@Composable
245+
fun RenderInput(ir: NanoIR, modifier: Modifier = Modifier) {
189246
val placeholder = ir.props["placeholder"]?.jsonPrimitive?.content ?: ""
190247

191248
OutlinedTextField(
192249
value = "",
193250
onValueChange = { },
194251
placeholder = { Text(placeholder) },
195-
modifier = modifier.fillMaxWidth()
252+
modifier = modifier.fillMaxWidth(),
253+
singleLine = true
196254
)
197255
}
198256

199257
@Composable
200-
private fun RenderCheckbox(ir: NanoIR, modifier: Modifier) {
258+
fun RenderCheckbox(ir: NanoIR, modifier: Modifier = Modifier) {
201259
Row(
202260
modifier = modifier,
203261
verticalAlignment = Alignment.CenterVertically
@@ -207,37 +265,92 @@ object ComposeNanoRenderer {
207265
}
208266

209267
@Composable
210-
private fun RenderConditional(ir: NanoIR, modifier: Modifier) {
268+
fun RenderTextArea(ir: NanoIR, modifier: Modifier = Modifier) {
269+
val placeholder = ir.props["placeholder"]?.jsonPrimitive?.content ?: ""
270+
val rows = ir.props["rows"]?.jsonPrimitive?.content?.toIntOrNull() ?: 4
271+
272+
OutlinedTextField(
273+
value = "",
274+
onValueChange = { },
275+
placeholder = { Text(placeholder) },
276+
modifier = modifier
277+
.fillMaxWidth()
278+
.height((rows * 24 + 32).dp),
279+
minLines = rows,
280+
maxLines = rows
281+
)
282+
}
283+
284+
@Composable
285+
fun RenderSelect(ir: NanoIR, modifier: Modifier = Modifier) {
286+
val placeholder = ir.props["placeholder"]?.jsonPrimitive?.content ?: "Select..."
287+
288+
// Simple dropdown placeholder - in real app would use ExposedDropdownMenuBox
289+
Box(
290+
modifier = modifier
291+
.fillMaxWidth()
292+
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(4.dp))
293+
.padding(horizontal = 12.dp, vertical = 16.dp)
294+
) {
295+
Text(
296+
text = placeholder,
297+
color = MaterialTheme.colorScheme.onSurfaceVariant
298+
)
299+
}
300+
}
301+
302+
// ============================================================================
303+
// Control Flow Components
304+
// ============================================================================
305+
306+
@Composable
307+
fun RenderConditional(ir: NanoIR, modifier: Modifier = Modifier) {
211308
// Render the then branch (conditional evaluation happens at runtime)
212309
// In static preview, we just render children directly
213310
Column(modifier = modifier) {
214311
ir.children?.forEach { child ->
215-
Render(child)
312+
RenderNode(child)
216313
}
217314
}
218315
}
219316

220317
@Composable
221-
private fun RenderForLoop(ir: NanoIR, modifier: Modifier) {
318+
fun RenderForLoop(ir: NanoIR, modifier: Modifier = Modifier) {
222319
// Render the loop body once as a preview
223320
// In static preview, show a single iteration of the loop
224321
Column(modifier = modifier) {
225322
ir.children?.forEach { child ->
226-
Render(child)
323+
RenderNode(child)
324+
}
325+
}
326+
}
327+
328+
// ============================================================================
329+
// Meta Components
330+
// ============================================================================
331+
332+
@Composable
333+
fun RenderComponent(ir: NanoIR, modifier: Modifier = Modifier) {
334+
Column(modifier = modifier) {
335+
ir.children?.forEach { child ->
336+
RenderNode(child)
227337
}
228338
}
229339
}
230340

231341
@Composable
232-
private fun RenderUnknown(ir: NanoIR, modifier: Modifier) {
342+
fun RenderUnknown(ir: NanoIR, modifier: Modifier = Modifier) {
233343
Text(
234344
text = "Unknown: ${ir.type}",
235345
modifier = modifier,
236346
color = MaterialTheme.colorScheme.error
237347
)
238348
}
239349

240-
// Extension functions for parsing spacing/alignment values
350+
// ============================================================================
351+
// Extension Functions
352+
// ============================================================================
353+
241354
private fun String.toSpacing() = when (this) {
242355
"xs" -> 4.dp
243356
"sm" -> 8.dp

0 commit comments

Comments
 (0)