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 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.
@@ -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: true },
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
- lines.push(`${INDENT}${cppPropType(p.type)} ${p.name};`);
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
- lines.push(`${indent} float _dd_payload = static_cast<float>(${payload});`);
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}, &${node.valueExpr});`);
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.indexVar;
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
- lines.push(`${indent}${INDENT}p.${k} = ${v};`);
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}, &${node.valueExpr}, ${min}, ${max});`);
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}, &${node.valueExpr}, ${node.min}, ${node.max});`);
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}, &${node.valueExpr}, ${speed});`);
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}, &${node.valueExpr}, ${speed});`);
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}, &${node.valueExpr}, ${varName}, ${count});`);
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}, &${node.valueExpr});`);
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}, &${node.valueExpr});`);
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
- lines.push(`${indent}imx::renderer::color_edit(${label}, ${node.valueExpr}.data());`);
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}, &${node.valueExpr}, ${varName}, ${count});`);
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}, &${node.valueExpr}, ${node.index});`);
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
- lines.push(`${indent}imx::renderer::color_picker(${label}, ${node.valueExpr}.data());`);
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: (v: string) => void; label?: string; placeholder?: string; style?: Style; }
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;
@@ -8,6 +8,7 @@ interface LoweringContext {
8
8
  propsParam: string | null;
9
9
  propsFieldTypes: Map<string, IRType | 'callback'>;
10
10
  bufferIndex: number;
11
+ mapCounter: number;
11
12
  sourceFile: ts.SourceFile;
12
13
  customComponents: Map<string, string>;
13
14
  }
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
- body.push({ kind: 'text_input', label, bufferIndex, stateVar, style, loc });
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
- key: indexVar,
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.2",
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
+ }