imxc 0.5.2 → 0.5.3
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/dist/compile.d.ts +2 -0
- package/dist/compile.js +84 -2
- package/dist/components.js +1 -1
- package/dist/emitter.d.ts +2 -2
- package/dist/emitter.js +90 -18
- package/dist/init.js +1 -1
- package/dist/ir.d.ts +4 -0
- package/dist/lowering.d.ts +1 -0
- package/dist/lowering.js +6 -11
- package/package.json +27 -27
package/dist/compile.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IRNode } from './ir.js';
|
|
1
2
|
export interface CompileResult {
|
|
2
3
|
success: boolean;
|
|
3
4
|
componentCount: number;
|
|
@@ -8,3 +9,4 @@ export interface CompileResult {
|
|
|
8
9
|
* Returns a result object instead of calling process.exit().
|
|
9
10
|
*/
|
|
10
11
|
export declare function compile(files: string[], outputDir: string): CompileResult;
|
|
12
|
+
export declare function resolveDragDropTypes(nodes: IRNode[]): void;
|
package/dist/compile.js
CHANGED
|
@@ -49,6 +49,7 @@ export function compile(files, outputDir) {
|
|
|
49
49
|
ir,
|
|
50
50
|
imports,
|
|
51
51
|
hasProps: ir.params.length > 0 || !!ir.namedPropsType,
|
|
52
|
+
boundProps: new Set(),
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
if (hasErrors) {
|
|
@@ -62,6 +63,15 @@ export function compile(files, outputDir) {
|
|
|
62
63
|
// Phase 3: Resolve imported component stateCount/bufferCount in IR, then emit
|
|
63
64
|
for (const comp of compiled) {
|
|
64
65
|
resolveCustomComponents(comp.ir.body, componentMap);
|
|
66
|
+
resolveDragDropTypes(comp.ir.body);
|
|
67
|
+
comp.boundProps = detectBoundProps(comp.ir.body);
|
|
68
|
+
}
|
|
69
|
+
// Build boundProps map for cross-component emitter use
|
|
70
|
+
const boundPropsMap = new Map();
|
|
71
|
+
for (const comp of compiled) {
|
|
72
|
+
boundPropsMap.set(comp.name, comp.boundProps);
|
|
73
|
+
}
|
|
74
|
+
for (const comp of compiled) {
|
|
65
75
|
const importInfos = [];
|
|
66
76
|
for (const [importedName] of comp.imports) {
|
|
67
77
|
const importedComp = componentMap.get(importedName);
|
|
@@ -72,7 +82,7 @@ export function compile(files, outputDir) {
|
|
|
72
82
|
});
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
const cppOutput = emitComponent(comp.ir, importInfos, comp.sourceFile);
|
|
85
|
+
const cppOutput = emitComponent(comp.ir, importInfos, comp.sourceFile, comp.boundProps, boundPropsMap);
|
|
76
86
|
const baseName = comp.name;
|
|
77
87
|
const outPath = path.join(outputDir, `${baseName}.gen.cpp`);
|
|
78
88
|
fs.writeFileSync(outPath, cppOutput);
|
|
@@ -86,7 +96,7 @@ export function compile(files, outputDir) {
|
|
|
86
96
|
// Only generate a header for inline props (not for named interface types —
|
|
87
97
|
// those are declared in the user's main.cpp)
|
|
88
98
|
if (comp.hasProps && !comp.ir.namedPropsType) {
|
|
89
|
-
const headerOutput = emitComponentHeader(comp.ir, comp.sourceFile);
|
|
99
|
+
const headerOutput = emitComponentHeader(comp.ir, comp.sourceFile, comp.boundProps);
|
|
90
100
|
const headerPath = path.join(outputDir, `${baseName}.gen.h`);
|
|
91
101
|
fs.writeFileSync(headerPath, headerOutput);
|
|
92
102
|
console.log(` ${baseName} -> ${headerPath} (header)`);
|
|
@@ -126,6 +136,54 @@ function resolveCustomComponents(nodes, map) {
|
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
}
|
|
139
|
+
export function resolveDragDropTypes(nodes) {
|
|
140
|
+
const typeMap = new Map();
|
|
141
|
+
collectDragDropTypes(nodes, typeMap);
|
|
142
|
+
applyDragDropTypes(nodes, typeMap);
|
|
143
|
+
}
|
|
144
|
+
function collectDragDropTypes(nodes, typeMap) {
|
|
145
|
+
for (const node of nodes) {
|
|
146
|
+
if (node.kind === 'begin_container' && node.tag === 'DragDropTarget') {
|
|
147
|
+
const onDrop = node.props['onDrop'] ?? '';
|
|
148
|
+
const parts = onDrop.split('|');
|
|
149
|
+
if (parts.length >= 3) {
|
|
150
|
+
const cppType = parts[0];
|
|
151
|
+
const typeStr = node.props['type'] ?? '';
|
|
152
|
+
const key = typeStr.replace(/^"|"$/g, '');
|
|
153
|
+
if (key)
|
|
154
|
+
typeMap.set(key, cppType);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (node.kind === 'conditional') {
|
|
158
|
+
collectDragDropTypes(node.body, typeMap);
|
|
159
|
+
if (node.elseBody)
|
|
160
|
+
collectDragDropTypes(node.elseBody, typeMap);
|
|
161
|
+
}
|
|
162
|
+
else if (node.kind === 'list_map') {
|
|
163
|
+
collectDragDropTypes(node.body, typeMap);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function applyDragDropTypes(nodes, typeMap) {
|
|
168
|
+
for (const node of nodes) {
|
|
169
|
+
if (node.kind === 'begin_container' && node.tag === 'DragDropSource') {
|
|
170
|
+
const typeStr = node.props['type'] ?? '';
|
|
171
|
+
const key = typeStr.replace(/^"|"$/g, '');
|
|
172
|
+
const cppType = typeMap.get(key);
|
|
173
|
+
if (cppType) {
|
|
174
|
+
node.props['_payloadType'] = cppType;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (node.kind === 'conditional') {
|
|
178
|
+
applyDragDropTypes(node.body, typeMap);
|
|
179
|
+
if (node.elseBody)
|
|
180
|
+
applyDragDropTypes(node.elseBody, typeMap);
|
|
181
|
+
}
|
|
182
|
+
else if (node.kind === 'list_map') {
|
|
183
|
+
applyDragDropTypes(node.body, typeMap);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
129
187
|
function collectEmbedImages(nodes) {
|
|
130
188
|
const images = [];
|
|
131
189
|
for (const node of nodes) {
|
|
@@ -177,6 +235,30 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
177
235
|
console.log(` ${rawSrc} -> ${headerPath} (embed)`);
|
|
178
236
|
}
|
|
179
237
|
}
|
|
238
|
+
function detectBoundProps(nodes) {
|
|
239
|
+
const bound = new Set();
|
|
240
|
+
walkNodesForBinding(nodes, bound);
|
|
241
|
+
return bound;
|
|
242
|
+
}
|
|
243
|
+
function walkNodesForBinding(nodes, bound) {
|
|
244
|
+
for (const node of nodes) {
|
|
245
|
+
if ('directBind' in node && node.directBind && 'valueExpr' in node) {
|
|
246
|
+
const expr = node.valueExpr;
|
|
247
|
+
if (expr && expr.startsWith('props.')) {
|
|
248
|
+
const propName = expr.slice(6).split('.')[0].split('[')[0];
|
|
249
|
+
bound.add(propName);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (node.kind === 'conditional') {
|
|
253
|
+
walkNodesForBinding(node.body, bound);
|
|
254
|
+
if (node.elseBody)
|
|
255
|
+
walkNodesForBinding(node.elseBody, bound);
|
|
256
|
+
}
|
|
257
|
+
else if (node.kind === 'list_map') {
|
|
258
|
+
walkNodesForBinding(node.body, bound);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
180
262
|
/**
|
|
181
263
|
* Parse the imx.d.ts in the given directory (if present) and extract
|
|
182
264
|
* all interface declarations as a map from interface name -> field name -> type.
|
package/dist/components.js
CHANGED
|
@@ -42,7 +42,7 @@ export const HOST_COMPONENTS = {
|
|
|
42
42
|
TextInput: {
|
|
43
43
|
props: {
|
|
44
44
|
value: { type: 'string', required: true },
|
|
45
|
-
onChange: { type: 'callback', required:
|
|
45
|
+
onChange: { type: 'callback', required: false },
|
|
46
46
|
label: { type: 'string', required: false },
|
|
47
47
|
placeholder: { type: 'string', required: false },
|
|
48
48
|
style: { type: 'style', required: false },
|
package/dist/emitter.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import type { IRComponent } from './ir.js';
|
|
|
3
3
|
* Emit a .gen.h header for a component that has props.
|
|
4
4
|
* Contains the props struct and function forward declaration.
|
|
5
5
|
*/
|
|
6
|
-
export declare function emitComponentHeader(comp: IRComponent, sourceFile?: string): string;
|
|
6
|
+
export declare function emitComponentHeader(comp: IRComponent, sourceFile?: string, boundProps?: Set<string>): string;
|
|
7
7
|
export interface ImportInfo {
|
|
8
8
|
name: string;
|
|
9
9
|
headerFile: string;
|
|
10
10
|
}
|
|
11
|
-
export declare function emitComponent(comp: IRComponent, imports?: ImportInfo[], sourceFile?: string): string;
|
|
11
|
+
export declare function emitComponent(comp: IRComponent, imports?: ImportInfo[], sourceFile?: string, boundProps?: Set<string>, boundPropsMap?: Map<string, Set<string>>): string;
|
|
12
12
|
export declare function emitRoot(rootName: string, stateCount: number, bufferCount: number, sourceFile?: string, propsType?: string, namedPropsType?: boolean): string;
|
package/dist/emitter.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const INDENT = ' ';
|
|
2
2
|
let currentCompName = '';
|
|
3
|
+
let currentBoundProps = new Set();
|
|
4
|
+
let allBoundProps = new Map();
|
|
3
5
|
function emitLocComment(loc, tag, lines, indent) {
|
|
4
6
|
if (loc) {
|
|
5
7
|
lines.push(`${indent}// ${loc.file}:${loc.line} <${tag}>`);
|
|
@@ -34,6 +36,19 @@ function asCharPtr(expr) {
|
|
|
34
36
|
// Expression — assume std::string, add .c_str()
|
|
35
37
|
return `${expr}.c_str()`;
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* For directBind emitters: if the valueExpr references a bound prop (pointer),
|
|
41
|
+
* return &*expr for bound props (C++ identity: &*ptr == ptr, and the *
|
|
42
|
+
* blocks the post-processing regex lookbehind from dereferencing again).
|
|
43
|
+
* Otherwise, emit &expr.
|
|
44
|
+
*/
|
|
45
|
+
function emitDirectBindPtr(valueExpr) {
|
|
46
|
+
const propName = valueExpr.startsWith('props.') ? valueExpr.slice(6).split('.')[0].split('[')[0] : '';
|
|
47
|
+
if (currentBoundProps.has(propName)) {
|
|
48
|
+
return `&*${valueExpr}`; // already a pointer; &* is identity, * blocks regex lookbehind
|
|
49
|
+
}
|
|
50
|
+
return `&${valueExpr}`;
|
|
51
|
+
}
|
|
37
52
|
function emitImVec4(arrayStr) {
|
|
38
53
|
const parts = arrayStr.split(',').map(s => {
|
|
39
54
|
const v = s.trim();
|
|
@@ -96,7 +111,7 @@ function emitDockSetupFunction(layout, compName, lines) {
|
|
|
96
111
|
* Emit a .gen.h header for a component that has props.
|
|
97
112
|
* Contains the props struct and function forward declaration.
|
|
98
113
|
*/
|
|
99
|
-
export function emitComponentHeader(comp, sourceFile) {
|
|
114
|
+
export function emitComponentHeader(comp, sourceFile, boundProps) {
|
|
100
115
|
const lines = [];
|
|
101
116
|
if (sourceFile) {
|
|
102
117
|
lines.push(`// Generated from ${sourceFile} by imxc`);
|
|
@@ -110,7 +125,12 @@ export function emitComponentHeader(comp, sourceFile) {
|
|
|
110
125
|
// Props struct
|
|
111
126
|
lines.push(`struct ${comp.name}Props {`);
|
|
112
127
|
for (const p of comp.params) {
|
|
113
|
-
|
|
128
|
+
if (boundProps && boundProps.has(p.name)) {
|
|
129
|
+
lines.push(`${INDENT}${cppPropType(p.type)}* ${p.name} = nullptr;`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push(`${INDENT}${cppPropType(p.type)} ${p.name};`);
|
|
133
|
+
}
|
|
114
134
|
}
|
|
115
135
|
lines.push('};');
|
|
116
136
|
lines.push('');
|
|
@@ -119,7 +139,7 @@ export function emitComponentHeader(comp, sourceFile) {
|
|
|
119
139
|
lines.push('');
|
|
120
140
|
return lines.join('\n');
|
|
121
141
|
}
|
|
122
|
-
export function emitComponent(comp, imports, sourceFile) {
|
|
142
|
+
export function emitComponent(comp, imports, sourceFile, boundProps, boundPropsMap) {
|
|
123
143
|
const lines = [];
|
|
124
144
|
// Reset counters for each component
|
|
125
145
|
styleCounter = 0;
|
|
@@ -132,6 +152,8 @@ export function emitComponent(comp, imports, sourceFile) {
|
|
|
132
152
|
dragDropSourceStack.length = 0;
|
|
133
153
|
dragDropTargetStack.length = 0;
|
|
134
154
|
currentCompName = comp.name;
|
|
155
|
+
currentBoundProps = boundProps ?? new Set();
|
|
156
|
+
allBoundProps = boundPropsMap ?? new Map();
|
|
135
157
|
// hasProps: true for inline prop struct OR named interface
|
|
136
158
|
const hasProps = comp.params.length > 0 || !!comp.namedPropsType;
|
|
137
159
|
// propsTypeName: for named interface use it directly; for inline use ComponentProps convention
|
|
@@ -227,6 +249,18 @@ export function emitComponent(comp, imports, sourceFile) {
|
|
|
227
249
|
}
|
|
228
250
|
// Body IR nodes
|
|
229
251
|
emitNodes(comp.body, lines, 1);
|
|
252
|
+
// Post-processing: dereference bound prop reads in expressions
|
|
253
|
+
if (currentBoundProps.size > 0) {
|
|
254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
255
|
+
if (lines[i].trimStart().startsWith('//'))
|
|
256
|
+
continue;
|
|
257
|
+
for (const prop of currentBoundProps) {
|
|
258
|
+
// Replace props.X reads with (*props.X) — but not &props.X or *props.X (already handled)
|
|
259
|
+
const pattern = new RegExp(`(?<![&*])\\bprops\\.${prop}\\b`, 'g');
|
|
260
|
+
lines[i] = lines[i].replace(pattern, `(*props.${prop})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
230
264
|
lines.push('}');
|
|
231
265
|
lines.push('');
|
|
232
266
|
return lines.join('\n');
|
|
@@ -938,7 +972,8 @@ function emitEndContainer(node, lines, indent) {
|
|
|
938
972
|
const payload = props['payload'] ?? '0';
|
|
939
973
|
lines.push(`${indent}ImGui::EndGroup();`);
|
|
940
974
|
lines.push(`${indent}if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {`);
|
|
941
|
-
|
|
975
|
+
const payloadType = props['_payloadType'] ?? 'float';
|
|
976
|
+
lines.push(`${indent} ${payloadType} _dd_payload = static_cast<${payloadType}>(${payload});`);
|
|
942
977
|
lines.push(`${indent} ImGui::SetDragDropPayload(${typeStr}, &_dd_payload, sizeof(_dd_payload));`);
|
|
943
978
|
lines.push(`${indent} ImGui::Text("Dragging...");`);
|
|
944
979
|
lines.push(`${indent} ImGui::EndDragDropSource();`);
|
|
@@ -1043,6 +1078,30 @@ function emitTextInput(node, lines, indent) {
|
|
|
1043
1078
|
lines.push(`${indent}${INDENT}}`);
|
|
1044
1079
|
lines.push(`${indent}}`);
|
|
1045
1080
|
}
|
|
1081
|
+
else if (node.directBind && node.valueExpr) {
|
|
1082
|
+
const propName = node.valueExpr.startsWith('props.') ? node.valueExpr.slice(6).split('.')[0].split('[')[0] : '';
|
|
1083
|
+
const isBound = currentBoundProps.has(propName);
|
|
1084
|
+
const readExpr = isBound ? `(*${node.valueExpr})` : node.valueExpr;
|
|
1085
|
+
const writeExpr = isBound ? `(*${node.valueExpr})` : node.valueExpr;
|
|
1086
|
+
lines.push(`${indent}{`);
|
|
1087
|
+
lines.push(`${indent}${INDENT}auto& buf = ctx.get_buffer(${node.bufferIndex});`);
|
|
1088
|
+
lines.push(`${indent}${INDENT}buf.sync_from(${readExpr});`);
|
|
1089
|
+
lines.push(`${indent}${INDENT}if (imx::renderer::text_input(${label}, buf)) {`);
|
|
1090
|
+
lines.push(`${indent}${INDENT}${INDENT}${writeExpr} = buf.value();`);
|
|
1091
|
+
lines.push(`${indent}${INDENT}}`);
|
|
1092
|
+
lines.push(`${indent}}`);
|
|
1093
|
+
}
|
|
1094
|
+
else if (node.valueExpr !== undefined) {
|
|
1095
|
+
lines.push(`${indent}{`);
|
|
1096
|
+
lines.push(`${indent}${INDENT}auto& buf = ctx.get_buffer(${node.bufferIndex});`);
|
|
1097
|
+
lines.push(`${indent}${INDENT}buf.sync_from(${node.valueExpr});`);
|
|
1098
|
+
lines.push(`${indent}${INDENT}if (imx::renderer::text_input(${label}, buf)) {`);
|
|
1099
|
+
if (node.onChangeExpr) {
|
|
1100
|
+
lines.push(`${indent}${INDENT}${INDENT}${node.onChangeExpr};`);
|
|
1101
|
+
}
|
|
1102
|
+
lines.push(`${indent}${INDENT}}`);
|
|
1103
|
+
lines.push(`${indent}}`);
|
|
1104
|
+
}
|
|
1046
1105
|
else {
|
|
1047
1106
|
lines.push(`${indent}auto& buf_${node.bufferIndex} = ctx.get_buffer(${node.bufferIndex});`);
|
|
1048
1107
|
lines.push(`${indent}imx::renderer::text_input(${label}, buf_${node.bufferIndex});`);
|
|
@@ -1063,7 +1122,7 @@ function emitCheckbox(node, lines, indent) {
|
|
|
1063
1122
|
}
|
|
1064
1123
|
else if (node.directBind && node.valueExpr) {
|
|
1065
1124
|
// Direct pointer binding — no temp variable
|
|
1066
|
-
lines.push(`${indent}imx::renderer::checkbox(${label},
|
|
1125
|
+
lines.push(`${indent}imx::renderer::checkbox(${label}, ${emitDirectBindPtr(node.valueExpr)});`);
|
|
1067
1126
|
}
|
|
1068
1127
|
else if (node.valueExpr !== undefined) {
|
|
1069
1128
|
// Props-bound / expression-bound case
|
|
@@ -1096,9 +1155,10 @@ function emitListMap(node, lines, indent, depth) {
|
|
|
1096
1155
|
if (node.loc) {
|
|
1097
1156
|
lines.push(`${indent}// ${node.loc.file}:${node.loc.line} .map()`);
|
|
1098
1157
|
}
|
|
1099
|
-
const idx = node.
|
|
1158
|
+
const idx = node.internalIndexVar;
|
|
1100
1159
|
lines.push(`${indent}for (size_t ${idx} = 0; ${idx} < ${node.array}.size(); ${idx}++) {`);
|
|
1101
1160
|
lines.push(`${indent}${INDENT}auto& ${node.itemVar} = ${node.array}[${idx}];`);
|
|
1161
|
+
lines.push(`${indent}${INDENT}size_t ${node.indexVar} = ${idx};`);
|
|
1102
1162
|
lines.push(`${indent}${INDENT}ctx.begin_instance("${node.componentName}", (int)${idx}, ${node.stateCount}, ${node.bufferCount});`);
|
|
1103
1163
|
emitNodes(node.body, lines, depth + 2);
|
|
1104
1164
|
lines.push(`${indent}${INDENT}ctx.end_instance();`);
|
|
@@ -1108,13 +1168,19 @@ function emitCustomComponent(node, lines, indent) {
|
|
|
1108
1168
|
emitLocComment(node.loc, node.name, lines, indent);
|
|
1109
1169
|
const instanceIndex = customComponentCounter++;
|
|
1110
1170
|
const propsEntries = Object.entries(node.props);
|
|
1171
|
+
const childBound = allBoundProps.get(node.name) ?? new Set();
|
|
1111
1172
|
lines.push(`${indent}ctx.begin_instance("${node.name}", ${instanceIndex}, ${node.stateCount}, ${node.bufferCount});`);
|
|
1112
1173
|
if (propsEntries.length > 0) {
|
|
1113
1174
|
// MSVC-compatible: use variable-based prop assignment instead of designated initializers
|
|
1114
1175
|
lines.push(`${indent}{`);
|
|
1115
1176
|
lines.push(`${indent}${INDENT}${node.name}Props p;`);
|
|
1116
1177
|
for (const [k, v] of propsEntries) {
|
|
1117
|
-
|
|
1178
|
+
if (childBound.has(k)) {
|
|
1179
|
+
lines.push(`${indent}${INDENT}p.${k} = &${v};`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
lines.push(`${indent}${INDENT}p.${k} = ${v};`);
|
|
1183
|
+
}
|
|
1118
1184
|
}
|
|
1119
1185
|
lines.push(`${indent}${INDENT}${node.name}_render(ctx, p);`);
|
|
1120
1186
|
lines.push(`${indent}}`);
|
|
@@ -1163,7 +1229,7 @@ function emitSliderFloat(node, lines, indent) {
|
|
|
1163
1229
|
}
|
|
1164
1230
|
else if (node.directBind && node.valueExpr) {
|
|
1165
1231
|
// Direct pointer binding
|
|
1166
|
-
lines.push(`${indent}imx::renderer::slider_float(${label},
|
|
1232
|
+
lines.push(`${indent}imx::renderer::slider_float(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${min}, ${max});`);
|
|
1167
1233
|
}
|
|
1168
1234
|
else if (node.valueExpr !== undefined) {
|
|
1169
1235
|
lines.push(`${indent}{`);
|
|
@@ -1188,7 +1254,7 @@ function emitSliderInt(node, lines, indent) {
|
|
|
1188
1254
|
lines.push(`${indent}}`);
|
|
1189
1255
|
}
|
|
1190
1256
|
else if (node.directBind && node.valueExpr) {
|
|
1191
|
-
lines.push(`${indent}imx::renderer::slider_int(${label},
|
|
1257
|
+
lines.push(`${indent}imx::renderer::slider_int(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${node.min}, ${node.max});`);
|
|
1192
1258
|
}
|
|
1193
1259
|
else if (node.valueExpr !== undefined) {
|
|
1194
1260
|
lines.push(`${indent}{`);
|
|
@@ -1214,7 +1280,7 @@ function emitDragFloat(node, lines, indent) {
|
|
|
1214
1280
|
lines.push(`${indent}}`);
|
|
1215
1281
|
}
|
|
1216
1282
|
else if (node.directBind && node.valueExpr) {
|
|
1217
|
-
lines.push(`${indent}imx::renderer::drag_float(${label},
|
|
1283
|
+
lines.push(`${indent}imx::renderer::drag_float(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${speed});`);
|
|
1218
1284
|
}
|
|
1219
1285
|
else if (node.valueExpr !== undefined) {
|
|
1220
1286
|
lines.push(`${indent}{`);
|
|
@@ -1240,7 +1306,7 @@ function emitDragInt(node, lines, indent) {
|
|
|
1240
1306
|
lines.push(`${indent}}`);
|
|
1241
1307
|
}
|
|
1242
1308
|
else if (node.directBind && node.valueExpr) {
|
|
1243
|
-
lines.push(`${indent}imx::renderer::drag_int(${label},
|
|
1309
|
+
lines.push(`${indent}imx::renderer::drag_int(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${speed});`);
|
|
1244
1310
|
}
|
|
1245
1311
|
else if (node.valueExpr !== undefined) {
|
|
1246
1312
|
lines.push(`${indent}{`);
|
|
@@ -1271,7 +1337,7 @@ function emitCombo(node, lines, indent) {
|
|
|
1271
1337
|
else if (node.directBind && node.valueExpr) {
|
|
1272
1338
|
lines.push(`${indent}{`);
|
|
1273
1339
|
lines.push(`${indent}${INDENT}const char* ${varName}[] = {${itemsList.join(', ')}};`);
|
|
1274
|
-
lines.push(`${indent}${INDENT}imx::renderer::combo(${label},
|
|
1340
|
+
lines.push(`${indent}${INDENT}imx::renderer::combo(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${varName}, ${count});`);
|
|
1275
1341
|
lines.push(`${indent}}`);
|
|
1276
1342
|
}
|
|
1277
1343
|
else if (node.valueExpr !== undefined) {
|
|
@@ -1298,7 +1364,7 @@ function emitInputInt(node, lines, indent) {
|
|
|
1298
1364
|
lines.push(`${indent}}`);
|
|
1299
1365
|
}
|
|
1300
1366
|
else if (node.directBind && node.valueExpr) {
|
|
1301
|
-
lines.push(`${indent}imx::renderer::input_int(${label},
|
|
1367
|
+
lines.push(`${indent}imx::renderer::input_int(${label}, ${emitDirectBindPtr(node.valueExpr)});`);
|
|
1302
1368
|
}
|
|
1303
1369
|
else if (node.valueExpr !== undefined) {
|
|
1304
1370
|
lines.push(`${indent}{`);
|
|
@@ -1323,7 +1389,7 @@ function emitInputFloat(node, lines, indent) {
|
|
|
1323
1389
|
lines.push(`${indent}}`);
|
|
1324
1390
|
}
|
|
1325
1391
|
else if (node.directBind && node.valueExpr) {
|
|
1326
|
-
lines.push(`${indent}imx::renderer::input_float(${label},
|
|
1392
|
+
lines.push(`${indent}imx::renderer::input_float(${label}, ${emitDirectBindPtr(node.valueExpr)});`);
|
|
1327
1393
|
}
|
|
1328
1394
|
else if (node.valueExpr !== undefined) {
|
|
1329
1395
|
lines.push(`${indent}{`);
|
|
@@ -1348,7 +1414,10 @@ function emitColorEdit(node, lines, indent) {
|
|
|
1348
1414
|
lines.push(`${indent}}`);
|
|
1349
1415
|
}
|
|
1350
1416
|
else if (node.directBind && node.valueExpr) {
|
|
1351
|
-
|
|
1417
|
+
const propName = node.valueExpr.startsWith('props.') ? node.valueExpr.slice(6).split('.')[0].split('[')[0] : '';
|
|
1418
|
+
const isBound = currentBoundProps.has(propName);
|
|
1419
|
+
const dataExpr = isBound ? `(${node.valueExpr})->data()` : `${node.valueExpr}.data()`;
|
|
1420
|
+
lines.push(`${indent}imx::renderer::color_edit(${label}, ${dataExpr});`);
|
|
1352
1421
|
}
|
|
1353
1422
|
else if (node.valueExpr !== undefined) {
|
|
1354
1423
|
lines.push(`${indent}{`);
|
|
@@ -1379,7 +1448,7 @@ function emitListBox(node, lines, indent) {
|
|
|
1379
1448
|
else if (node.directBind && node.valueExpr) {
|
|
1380
1449
|
lines.push(`${indent}{`);
|
|
1381
1450
|
lines.push(`${indent}${INDENT}const char* ${varName}[] = {${itemsList.join(', ')}};`);
|
|
1382
|
-
lines.push(`${indent}${INDENT}imx::renderer::list_box(${label},
|
|
1451
|
+
lines.push(`${indent}${INDENT}imx::renderer::list_box(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${varName}, ${count});`);
|
|
1383
1452
|
lines.push(`${indent}}`);
|
|
1384
1453
|
}
|
|
1385
1454
|
else if (node.valueExpr !== undefined) {
|
|
@@ -1451,7 +1520,7 @@ function emitRadio(node, lines, indent) {
|
|
|
1451
1520
|
lines.push(`${indent}}`);
|
|
1452
1521
|
}
|
|
1453
1522
|
else if (node.directBind && node.valueExpr) {
|
|
1454
|
-
lines.push(`${indent}imx::renderer::radio(${label},
|
|
1523
|
+
lines.push(`${indent}imx::renderer::radio(${label}, ${emitDirectBindPtr(node.valueExpr)}, ${node.index});`);
|
|
1455
1524
|
}
|
|
1456
1525
|
else if (node.valueExpr !== undefined) {
|
|
1457
1526
|
lines.push(`${indent}{`);
|
|
@@ -1493,7 +1562,10 @@ function emitColorPicker(node, lines, indent) {
|
|
|
1493
1562
|
lines.push(`${indent}}`);
|
|
1494
1563
|
}
|
|
1495
1564
|
else if (node.directBind && node.valueExpr) {
|
|
1496
|
-
|
|
1565
|
+
const propName = node.valueExpr.startsWith('props.') ? node.valueExpr.slice(6).split('.')[0].split('[')[0] : '';
|
|
1566
|
+
const isBound = currentBoundProps.has(propName);
|
|
1567
|
+
const dataExpr = isBound ? `(${node.valueExpr})->data()` : `${node.valueExpr}.data()`;
|
|
1568
|
+
lines.push(`${indent}imx::renderer::color_picker(${label}, ${dataExpr});`);
|
|
1497
1569
|
}
|
|
1498
1570
|
else if (node.valueExpr !== undefined) {
|
|
1499
1571
|
lines.push(`${indent}{`);
|
package/dist/init.js
CHANGED
|
@@ -179,7 +179,7 @@ interface RowProps { gap?: number; style?: Style; children?: any; }
|
|
|
179
179
|
interface ColumnProps { gap?: number; style?: Style; children?: any; }
|
|
180
180
|
interface TextProps { style?: Style; children?: any; }
|
|
181
181
|
interface ButtonProps { title: string; onPress: () => void; disabled?: boolean; style?: Style; }
|
|
182
|
-
interface TextInputProps { value: string; onChange
|
|
182
|
+
interface TextInputProps { value: string; onChange?: (v: string) => void; label?: string; placeholder?: string; style?: Style; }
|
|
183
183
|
interface CheckboxProps { value: boolean; onChange?: (v: boolean) => void; label?: string; style?: Style; }
|
|
184
184
|
interface SeparatorProps {}
|
|
185
185
|
interface PopupProps { id: string; style?: Style; children?: any; }
|
package/dist/ir.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export interface IRTextInput {
|
|
|
57
57
|
label: string;
|
|
58
58
|
bufferIndex: number;
|
|
59
59
|
stateVar: string;
|
|
60
|
+
valueExpr?: string;
|
|
61
|
+
onChangeExpr?: string;
|
|
62
|
+
directBind?: boolean;
|
|
60
63
|
style?: string;
|
|
61
64
|
loc?: SourceLoc;
|
|
62
65
|
}
|
|
@@ -100,6 +103,7 @@ export interface IRListMap {
|
|
|
100
103
|
array: string;
|
|
101
104
|
itemVar: string;
|
|
102
105
|
indexVar: string;
|
|
106
|
+
internalIndexVar: string;
|
|
103
107
|
key: string;
|
|
104
108
|
componentName: string;
|
|
105
109
|
stateCount: number;
|
package/dist/lowering.d.ts
CHANGED
package/dist/lowering.js
CHANGED
|
@@ -65,6 +65,7 @@ export function lowerComponent(parsed, validation, externalInterfaces) {
|
|
|
65
65
|
propsParam,
|
|
66
66
|
propsFieldTypes,
|
|
67
67
|
bufferIndex: 0,
|
|
68
|
+
mapCounter: 0,
|
|
68
69
|
sourceFile: parsed.sourceFile,
|
|
69
70
|
customComponents: validation.customComponents,
|
|
70
71
|
};
|
|
@@ -642,17 +643,9 @@ function lowerButton(attrs, rawAttrs, body, ctx, loc) {
|
|
|
642
643
|
function lowerTextInput(attrs, rawAttrs, body, ctx, loc) {
|
|
643
644
|
const label = attrs['label'] ?? '""';
|
|
644
645
|
const bufferIndex = ctx.bufferIndex++;
|
|
645
|
-
// Detect bound state variable from value prop
|
|
646
|
-
let stateVar = '';
|
|
647
|
-
const valueExpr = rawAttrs.get('value');
|
|
648
|
-
if (valueExpr && ts.isIdentifier(valueExpr)) {
|
|
649
|
-
const varName = valueExpr.text;
|
|
650
|
-
if (ctx.stateVars.has(varName)) {
|
|
651
|
-
stateVar = varName;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
646
|
const style = attrs['style'];
|
|
655
|
-
|
|
647
|
+
const { stateVar, valueExpr, onChangeExpr, directBind } = lowerValueOnChange(rawAttrs, ctx);
|
|
648
|
+
body.push({ kind: 'text_input', label, bufferIndex, stateVar: stateVar, valueExpr, onChangeExpr, directBind, style, loc });
|
|
656
649
|
}
|
|
657
650
|
function lowerCheckbox(attrs, rawAttrs, body, ctx, loc) {
|
|
658
651
|
const label = attrs['label'] ?? '""';
|
|
@@ -862,12 +855,14 @@ function lowerListMap(node, body, ctx, loc) {
|
|
|
862
855
|
lowerJsxExpression(callback.body, mapBody, ctx);
|
|
863
856
|
}
|
|
864
857
|
}
|
|
858
|
+
const internalIndexVar = `_map_idx_${ctx.mapCounter++}`;
|
|
865
859
|
body.push({
|
|
866
860
|
kind: 'list_map',
|
|
867
861
|
array,
|
|
868
862
|
itemVar,
|
|
869
863
|
indexVar,
|
|
870
|
-
|
|
864
|
+
internalIndexVar,
|
|
865
|
+
key: internalIndexVar,
|
|
871
866
|
componentName: 'ListItem',
|
|
872
867
|
stateCount: 0,
|
|
873
868
|
bufferCount: 0,
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "imxc",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Compiler for IMX — compiles React-like .tsx to native Dear ImGui C++",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"imxc": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
14
|
-
"test": "vitest run",
|
|
15
|
-
"test:watch": "vitest",
|
|
16
|
-
"prepublishOnly": "npm run build"
|
|
17
|
-
},
|
|
18
|
-
"keywords": ["imgui", "react", "tsx", "native", "gui", "compiler", "codegen"],
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"typescript": "^5.8.0"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"vitest": "^3.1.0",
|
|
25
|
-
"@types/node": "^22.0.0"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "imxc",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Compiler for IMX — compiles React-like .tsx to native Dear ImGui C++",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"imxc": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["imgui", "react", "tsx", "native", "gui", "compiler", "codegen"],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"typescript": "^5.8.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^3.1.0",
|
|
25
|
+
"@types/node": "^22.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|