Skip to content

Commit 42c9513

Browse files
committed
compose WIP
1 parent e5a0875 commit 42c9513

File tree

19 files changed

+1247
-10
lines changed

19 files changed

+1247
-10
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const defaultPluginSettings: PluginSettings = {
2323
responsiveRoot: false,
2424
flutterGenerationMode: "snippet",
2525
swiftUIGenerationMode: "snippet",
26+
composeGenerationMode: "snippet",
2627
roundTailwindValues: true,
2728
roundTailwindColors: true,
2829
useColorVariables: true,

packages/backend/src/common/images.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,25 @@ export const PLACEHOLDER_IMAGE_DOMAIN = "https://placehold.co";
77

88
const createCanvasImageUrl = (width: number, height: number): string => {
99
// Check if we're in a browser environment
10-
if (typeof document === 'undefined' || typeof window === 'undefined') {
10+
console.log("typeof document", typeof document);
11+
if (typeof document === "undefined" || typeof window === "undefined") {
1112
// Fallback for non-browser environments
1213
return `${PLACEHOLDER_IMAGE_DOMAIN}/${width}x${height}`;
1314
}
1415

15-
const canvas = document.createElement('canvas');
16+
const canvas = document.createElement("canvas");
1617
canvas.width = width;
1718
canvas.height = height;
1819

19-
const ctx = canvas.getContext('2d');
20+
const ctx = canvas.getContext("2d");
2021
if (!ctx) {
2122
// Fallback if canvas context is not available
2223
return `${PLACEHOLDER_IMAGE_DOMAIN}/${width}x${height}`;
2324
}
2425

2526
const fontSize = Math.max(12, Math.floor(width * 0.15));
2627
ctx.font = `bold ${fontSize}px Inter, Arial, Helvetica, sans-serif`;
27-
ctx.fillStyle = '#888888';
28+
ctx.fillStyle = "#888888";
2829

2930
const text = `${width} x ${height}`;
3031
const textWidth = ctx.measureText(text).width;
@@ -42,19 +43,20 @@ const createCanvasImageUrl = (width: number, height: number): string => {
4243
}
4344
const byteArray = new Uint8Array(byteNumbers);
4445
const file = new Blob([byteArray], {
45-
type: 'image/png;base64',
46+
type: "image/png;base64",
4647
});
4748
return URL.createObjectURL(file);
4849
};
4950

5051
export const getPlaceholderImage = (w: number, h = -1, useCanvas = false) => {
5152
const _w = w.toFixed(0);
5253
const _h = (h < 0 ? w : h).toFixed(0);
53-
54+
55+
console.log("useCanvas", useCanvas);
5456
if (useCanvas) {
5557
return createCanvasImageUrl(parseInt(_w), parseInt(_h));
5658
}
57-
59+
5860
return `${PLACEHOLDER_IMAGE_DOMAIN}/${_w}x${_h}`;
5961
};
6062

packages/backend/src/common/retrieveUI/convertToCode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PluginSettings } from "types";
2+
import { composeMain } from "../../compose/composeMain";
23
import { flutterMain } from "../../flutter/flutterMain";
34
import { htmlMain } from "../../html/htmlMain";
45
import { swiftuiMain } from "../../swiftui/swiftuiMain";
@@ -15,6 +16,8 @@ export const convertToCode = async (
1516
return await flutterMain(nodes, settings);
1617
case "SwiftUI":
1718
return await swiftuiMain(nodes, settings);
19+
case "Compose":
20+
return composeMain(nodes, settings);
1821
case "HTML":
1922
default:
2023
return (await htmlMain(nodes, settings)).html;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
export const getMainAxisAlignment = (
2+
node: InferredAutoLayoutResult,
3+
): string => {
4+
switch (node.primaryAxisAlignItems) {
5+
case undefined:
6+
case "MIN":
7+
return "Arrangement.Start";
8+
case "CENTER":
9+
return "Arrangement.Center";
10+
case "MAX":
11+
return "Arrangement.End";
12+
case "SPACE_BETWEEN":
13+
return "Arrangement.SpaceBetween";
14+
}
15+
};
16+
17+
export const getCrossAxisAlignment = (
18+
node: InferredAutoLayoutResult,
19+
): string => {
20+
// For Row (horizontal layout), cross axis is vertical
21+
// For Column (vertical layout), cross axis is horizontal
22+
if (node.layoutMode === "HORIZONTAL") {
23+
switch (node.counterAxisAlignItems) {
24+
case undefined:
25+
case "MIN":
26+
return "Alignment.Top";
27+
case "CENTER":
28+
return "Alignment.CenterVertically";
29+
case "MAX":
30+
return "Alignment.Bottom";
31+
case "BASELINE":
32+
return "Alignment.CenterVertically"; // Compose doesn't have baseline alignment for Row
33+
}
34+
} else {
35+
// VERTICAL layout mode
36+
switch (node.counterAxisAlignItems) {
37+
case undefined:
38+
case "MIN":
39+
return "Alignment.Start";
40+
case "CENTER":
41+
return "Alignment.CenterHorizontally";
42+
case "MAX":
43+
return "Alignment.End";
44+
case "BASELINE":
45+
return "Alignment.CenterHorizontally"; // Baseline not applicable for Column
46+
}
47+
}
48+
};
49+
50+
export const getWrapAlignment = (
51+
node: InferredAutoLayoutResult,
52+
): string => {
53+
switch (node.primaryAxisAlignItems) {
54+
case undefined:
55+
case "MIN":
56+
return "Arrangement.Start";
57+
case "CENTER":
58+
return "Arrangement.Center";
59+
case "MAX":
60+
return "Arrangement.End";
61+
case "SPACE_BETWEEN":
62+
return "Arrangement.SpaceBetween";
63+
}
64+
};
65+
66+
export const getWrapRunAlignment = (
67+
node: InferredAutoLayoutResult,
68+
): string => {
69+
if (node.counterAxisAlignContent === "SPACE_BETWEEN") {
70+
return "Arrangement.SpaceBetween";
71+
}
72+
73+
// For FlowRow/FlowColumn, the cross axis alignment depends on layout mode
74+
if (node.layoutMode === "HORIZONTAL") {
75+
// FlowRow - vertical alignment
76+
switch (node.counterAxisAlignItems) {
77+
case undefined:
78+
case "MIN":
79+
return "Arrangement.Top";
80+
case "CENTER":
81+
case "BASELINE":
82+
return "Arrangement.Center";
83+
case "MAX":
84+
return "Arrangement.Bottom";
85+
}
86+
} else {
87+
// FlowColumn - horizontal alignment
88+
switch (node.counterAxisAlignItems) {
89+
case undefined:
90+
case "MIN":
91+
return "Arrangement.Start";
92+
case "CENTER":
93+
case "BASELINE":
94+
return "Arrangement.Center";
95+
case "MAX":
96+
return "Arrangement.End";
97+
}
98+
}
99+
};
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { AltNode } from "../../alt_api_types";
2+
import { numberToFixedString } from "../../common/numToAutoFixed";
3+
4+
/**
5+
* Handles opacity transformations for Jetpack Compose
6+
* Maps to Modifier.alpha() in Compose
7+
*/
8+
export const composeOpacity = (
9+
node: MinimalBlendMixin,
10+
child: string,
11+
): string => {
12+
if (node.opacity !== undefined && node.opacity !== 1 && child !== "") {
13+
const opacity = numberToFixedString(node.opacity);
14+
return `Box(
15+
modifier = Modifier.alpha(${opacity}f)
16+
) {
17+
${child}
18+
}`;
19+
}
20+
return child;
21+
};
22+
23+
/**
24+
* Handles visibility transformations for Jetpack Compose
25+
* Uses conditional rendering or alpha(0f) based on visibility
26+
*/
27+
export const composeVisibility = (node: SceneNode, child: string): string => {
28+
// [when testing] node.visible can be undefined
29+
if (node.visible !== undefined && !node.visible && child !== "") {
30+
// In Compose, we can either use conditional rendering or set alpha to 0
31+
// Using alpha(0f) to maintain layout space (similar to visibility: hidden in CSS)
32+
return `Box(
33+
modifier = Modifier.alpha(0f)
34+
) {
35+
${child}
36+
}`;
37+
}
38+
return child;
39+
};
40+
41+
/**
42+
* Handles rotation transformations for Jetpack Compose
43+
* Maps to Modifier.rotate() in Compose
44+
* Converts angles from degrees to the format expected by Compose
45+
*/
46+
export const composeRotation = (node: AltNode, child: string): string => {
47+
if (
48+
node.rotation !== undefined &&
49+
child !== "" &&
50+
Math.round(node.rotation) !== 0
51+
) {
52+
const totalRotation = (node.rotation || 0) + (node.cumulativeRotation || 0);
53+
54+
if (Math.round(totalRotation) === 0) {
55+
return child;
56+
}
57+
58+
const rotationDegrees = numberToFixedString(totalRotation);
59+
return `Box(
60+
modifier = Modifier.rotate(${rotationDegrees}f)
61+
) {
62+
${child}
63+
}`;
64+
}
65+
return child;
66+
};
67+
68+
/**
69+
* Combines multiple blend transformations into a single modifier chain
70+
* This is more efficient than nesting multiple Box composables
71+
*/
72+
export const composeBlendModifiers = (node: AltNode, child: string): string => {
73+
const modifiers: string[] = [];
74+
75+
// Add opacity modifier
76+
if (node.opacity !== undefined && node.opacity !== 1) {
77+
const opacity = numberToFixedString(node.opacity);
78+
modifiers.push(`alpha(${opacity}f)`);
79+
}
80+
81+
// Add visibility modifier (using alpha for invisible elements)
82+
if (node.visible !== undefined && !node.visible) {
83+
modifiers.push(`alpha(0f)`);
84+
}
85+
86+
// Add rotation modifier
87+
const totalRotation = (node.rotation || 0) + (node.cumulativeRotation || 0);
88+
if (Math.round(totalRotation) !== 0) {
89+
const rotationDegrees = numberToFixedString(totalRotation);
90+
modifiers.push(`rotate(${rotationDegrees}f)`);
91+
}
92+
93+
// If we have modifiers, wrap in Box with combined modifier chain
94+
if (modifiers.length > 0 && child !== "") {
95+
const modifierChain = `Modifier.${modifiers.join(".")}`;
96+
return `Box(
97+
modifier = ${modifierChain}
98+
) {
99+
${child}
100+
}`;
101+
}
102+
103+
return child;
104+
};

0 commit comments

Comments
 (0)