imxc 0.2.0 → 0.3.0

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/lowering.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import ts from 'typescript';
2
2
  import { HOST_COMPONENTS, isHostComponent } from './components.js';
3
+ function getLoc(node, ctx) {
4
+ const { line } = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart());
5
+ return { file: ctx.sourceFile.fileName, line: line + 1 };
6
+ }
3
7
  export function lowerComponent(parsed, validation) {
4
8
  const func = parsed.component;
5
9
  const name = func.name.text;
@@ -43,6 +47,7 @@ export function lowerComponent(parsed, validation) {
43
47
  propsParam,
44
48
  bufferIndex: 0,
45
49
  sourceFile: parsed.sourceFile,
50
+ customComponents: validation.customComponents,
46
51
  };
47
52
  // Find return statement and lower its JSX
48
53
  const body = [];
@@ -137,6 +142,9 @@ export function exprToCpp(node, ctx) {
137
142
  if (ctx.stateVars.has(name)) {
138
143
  return `${name}.get()`;
139
144
  }
145
+ if (name === 'resetLayout') {
146
+ return 'imx_reset_layout';
147
+ }
140
148
  return name;
141
149
  }
142
150
  // Property access (e.g., props.name)
@@ -153,7 +161,12 @@ export function exprToCpp(node, ctx) {
153
161
  if (ts.isBinaryExpression(node)) {
154
162
  const left = exprToCpp(node.left, ctx);
155
163
  const right = exprToCpp(node.right, ctx);
156
- const op = node.operatorToken.getText();
164
+ let op = node.operatorToken.getText();
165
+ // Map TypeScript operators to C++ equivalents
166
+ if (op === '===')
167
+ op = '==';
168
+ else if (op === '!==')
169
+ op = '!=';
157
170
  return `${left} ${op} ${right}`;
158
171
  }
159
172
  // Prefix unary expression (e.g., !show)
@@ -243,8 +256,13 @@ function extractActionStatements(expr, ctx) {
243
256
  return [exprToCpp(expr.body, ctx) + ';'];
244
257
  }
245
258
  }
246
- // If not an arrow function, just call it
247
- return [exprToCpp(expr, ctx) + ';'];
259
+ // If not an arrow function, call it
260
+ const code = exprToCpp(expr, ctx);
261
+ // Bare identifier (not already a call) needs () to invoke
262
+ if (ts.isIdentifier(expr)) {
263
+ return [code + '();'];
264
+ }
265
+ return [code + ';'];
248
266
  }
249
267
  /**
250
268
  * Lower a JSX expression (possibly wrapped in parenthesized expr) into IR nodes.
@@ -273,7 +291,7 @@ function lowerJsxExpression(node, body, ctx) {
273
291
  const condition = exprToCpp(node.left, ctx);
274
292
  const condBody = [];
275
293
  lowerJsxExpression(node.right, condBody, ctx);
276
- body.push({ kind: 'conditional', condition, body: condBody });
294
+ body.push({ kind: 'conditional', condition, body: condBody, loc: getLoc(node, ctx) });
277
295
  return;
278
296
  }
279
297
  // Ternary: condition ? <A/> : <B/>
@@ -283,12 +301,12 @@ function lowerJsxExpression(node, body, ctx) {
283
301
  const elseBody = [];
284
302
  lowerJsxExpression(node.whenTrue, thenBody, ctx);
285
303
  lowerJsxExpression(node.whenFalse, elseBody, ctx);
286
- body.push({ kind: 'conditional', condition, body: thenBody, elseBody });
304
+ body.push({ kind: 'conditional', condition, body: thenBody, elseBody, loc: getLoc(node, ctx) });
287
305
  return;
288
306
  }
289
307
  // items.map(item => <Comp/>)
290
308
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
291
- lowerListMap(node, body, ctx);
309
+ lowerListMap(node, body, ctx, getLoc(node, ctx));
292
310
  return;
293
311
  }
294
312
  // JsxExpression wrapper
@@ -303,7 +321,15 @@ function lowerJsxElement(node, body, ctx) {
303
321
  return;
304
322
  const name = tagName.text;
305
323
  if (name === 'Text') {
306
- lowerTextElement(node, body, ctx);
324
+ lowerTextElement(node, body, ctx, getLoc(node, ctx));
325
+ return;
326
+ }
327
+ if (name === 'BulletText') {
328
+ lowerBulletTextElement(node, body, ctx, getLoc(node, ctx));
329
+ return;
330
+ }
331
+ if (name === 'DockLayout') {
332
+ body.push(lowerDockLayout(node, ctx));
307
333
  return;
308
334
  }
309
335
  if (isHostComponent(name)) {
@@ -311,7 +337,48 @@ function lowerJsxElement(node, body, ctx) {
311
337
  const attrs = getAttributes(node.openingElement.attributes, ctx);
312
338
  if (def.isContainer) {
313
339
  const containerTag = name;
314
- body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
340
+ // Special handling for DragDropTarget lower onDrop callback with type info
341
+ if (name === 'DragDropTarget') {
342
+ const rawAttrs = getRawAttributes(node.openingElement.attributes);
343
+ const props = {};
344
+ for (const [attrName, expr] of rawAttrs) {
345
+ if (attrName === 'onDrop' && expr && (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr))) {
346
+ const params = expr.parameters;
347
+ if (params.length > 0) {
348
+ const param = params[0];
349
+ const paramName = ts.isIdentifier(param.name) ? param.name.text : '_p';
350
+ let cppType = 'float';
351
+ if (param.type) {
352
+ const typeText = param.type.getText();
353
+ if (typeText === 'number')
354
+ cppType = 'float';
355
+ else if (typeText === 'boolean')
356
+ cppType = 'bool';
357
+ else if (typeText === 'string')
358
+ cppType = 'std::string';
359
+ }
360
+ const bodyCode = ts.isBlock(expr.body)
361
+ ? expr.body.statements.map(s => stmtToCpp(s, ctx)).join(' ')
362
+ : exprToCpp(expr.body, ctx) + ';';
363
+ // Store as structured string: type|paramName|body
364
+ props[attrName] = `${cppType}|${paramName}|${bodyCode}`;
365
+ }
366
+ }
367
+ else if (expr) {
368
+ props[attrName] = exprToCpp(expr, ctx);
369
+ }
370
+ else {
371
+ props[attrName] = 'true';
372
+ }
373
+ }
374
+ body.push({ kind: 'begin_container', tag: containerTag, props, loc: getLoc(node, ctx) });
375
+ for (const child of node.children) {
376
+ lowerJsxChild(child, body, ctx);
377
+ }
378
+ body.push({ kind: 'end_container', tag: containerTag });
379
+ return;
380
+ }
381
+ body.push({ kind: 'begin_container', tag: containerTag, props: attrs, loc: getLoc(node, ctx) });
315
382
  for (const child of node.children) {
316
383
  lowerJsxChild(child, body, ctx);
317
384
  }
@@ -321,7 +388,7 @@ function lowerJsxElement(node, body, ctx) {
321
388
  // Popup
322
389
  if (name === 'Popup') {
323
390
  const id = attrs['id'] ?? '';
324
- body.push({ kind: 'begin_popup', id });
391
+ body.push({ kind: 'begin_popup', id, loc: getLoc(node, ctx) });
325
392
  for (const child of node.children) {
326
393
  lowerJsxChild(child, body, ctx);
327
394
  }
@@ -331,7 +398,12 @@ function lowerJsxElement(node, body, ctx) {
331
398
  }
332
399
  // Custom component with children - treat as container-like (not common but handle gracefully)
333
400
  if (!isHostComponent(name)) {
334
- lowerCustomComponent(name, node.openingElement.attributes, body, ctx);
401
+ if (ctx.customComponents && ctx.customComponents.has(name)) {
402
+ lowerCustomComponent(name, node.openingElement.attributes, body, ctx, getLoc(node, ctx));
403
+ }
404
+ else {
405
+ lowerNativeWidget(name, node.openingElement.attributes, body, ctx, getLoc(node, ctx));
406
+ }
335
407
  return;
336
408
  }
337
409
  }
@@ -341,75 +413,143 @@ function lowerJsxSelfClosing(node, body, ctx) {
341
413
  return;
342
414
  const name = tagName.text;
343
415
  if (!isHostComponent(name)) {
344
- lowerCustomComponent(name, node.attributes, body, ctx);
416
+ if (ctx.customComponents && ctx.customComponents.has(name)) {
417
+ lowerCustomComponent(name, node.attributes, body, ctx, getLoc(node, ctx));
418
+ }
419
+ else {
420
+ lowerNativeWidget(name, node.attributes, body, ctx, getLoc(node, ctx));
421
+ }
345
422
  return;
346
423
  }
347
424
  const attrs = getAttributes(node.attributes, ctx);
348
425
  const rawAttrs = getRawAttributes(node.attributes);
426
+ const loc = getLoc(node, ctx);
349
427
  switch (name) {
350
428
  case 'Button':
351
- lowerButton(attrs, rawAttrs, body, ctx);
429
+ lowerButton(attrs, rawAttrs, body, ctx, loc);
352
430
  break;
353
431
  case 'TextInput':
354
- lowerTextInput(attrs, rawAttrs, body, ctx);
432
+ lowerTextInput(attrs, rawAttrs, body, ctx, loc);
355
433
  break;
356
434
  case 'Checkbox':
357
- lowerCheckbox(attrs, rawAttrs, body, ctx);
435
+ lowerCheckbox(attrs, rawAttrs, body, ctx, loc);
358
436
  break;
359
437
  case 'MenuItem':
360
- lowerMenuItem(attrs, rawAttrs, body, ctx);
438
+ lowerMenuItem(attrs, rawAttrs, body, ctx, loc);
361
439
  break;
362
440
  case 'SliderFloat':
363
- lowerSliderFloat(attrs, rawAttrs, body, ctx);
441
+ lowerSliderFloat(attrs, rawAttrs, body, ctx, loc);
364
442
  break;
365
443
  case 'SliderInt':
366
- lowerSliderInt(attrs, rawAttrs, body, ctx);
444
+ lowerSliderInt(attrs, rawAttrs, body, ctx, loc);
367
445
  break;
368
446
  case 'DragFloat':
369
- lowerDragFloat(attrs, rawAttrs, body, ctx);
447
+ lowerDragFloat(attrs, rawAttrs, body, ctx, loc);
370
448
  break;
371
449
  case 'DragInt':
372
- lowerDragInt(attrs, rawAttrs, body, ctx);
450
+ lowerDragInt(attrs, rawAttrs, body, ctx, loc);
373
451
  break;
374
452
  case 'Combo':
375
- lowerCombo(attrs, rawAttrs, body, ctx);
453
+ lowerCombo(attrs, rawAttrs, body, ctx, loc);
376
454
  break;
377
455
  case 'InputInt':
378
- lowerInputInt(attrs, rawAttrs, body, ctx);
456
+ lowerInputInt(attrs, rawAttrs, body, ctx, loc);
379
457
  break;
380
458
  case 'InputFloat':
381
- lowerInputFloat(attrs, rawAttrs, body, ctx);
459
+ lowerInputFloat(attrs, rawAttrs, body, ctx, loc);
382
460
  break;
383
461
  case 'ColorEdit':
384
- lowerColorEdit(attrs, rawAttrs, body, ctx);
462
+ lowerColorEdit(attrs, rawAttrs, body, ctx, loc);
385
463
  break;
386
464
  case 'ListBox':
387
- lowerListBox(attrs, rawAttrs, body, ctx);
465
+ lowerListBox(attrs, rawAttrs, body, ctx, loc);
388
466
  break;
389
467
  case 'ProgressBar':
390
- lowerProgressBar(attrs, rawAttrs, body, ctx);
468
+ lowerProgressBar(attrs, rawAttrs, body, ctx, loc);
391
469
  break;
392
470
  case 'Tooltip':
393
- lowerTooltip(attrs, body, ctx);
471
+ lowerTooltip(attrs, body, ctx, loc);
394
472
  break;
395
473
  case 'Separator':
396
- body.push({ kind: 'separator' });
474
+ body.push({ kind: 'separator', loc });
397
475
  break;
398
476
  case 'Text':
399
477
  // Self-closing <Text /> - empty text
400
- body.push({ kind: 'text', format: '', args: [] });
478
+ body.push({ kind: 'text', format: '', args: [], loc });
479
+ break;
480
+ case 'BulletText':
481
+ // Self-closing <BulletText /> - empty bullet
482
+ body.push({ kind: 'bullet_text', format: '', args: [], loc });
483
+ break;
484
+ case 'LabelText':
485
+ lowerLabelText(attrs, body, ctx, loc);
486
+ break;
487
+ case 'Selectable':
488
+ lowerSelectable(attrs, rawAttrs, body, ctx, loc);
489
+ break;
490
+ case 'Radio':
491
+ lowerRadio(attrs, rawAttrs, body, ctx, loc);
492
+ break;
493
+ case 'InputTextMultiline':
494
+ lowerInputTextMultiline(attrs, rawAttrs, body, ctx, loc);
495
+ break;
496
+ case 'ColorPicker':
497
+ lowerColorPicker(attrs, rawAttrs, body, ctx, loc);
498
+ break;
499
+ case 'PlotLines':
500
+ lowerPlotLines(attrs, body, ctx, loc);
401
501
  break;
502
+ case 'PlotHistogram':
503
+ lowerPlotHistogram(attrs, body, ctx, loc);
504
+ break;
505
+ case 'Image':
506
+ lowerImage(attrs, body, ctx, loc);
507
+ break;
508
+ case 'DrawLine': {
509
+ const p1 = attrs['p1'] ?? '0, 0';
510
+ const p2 = attrs['p2'] ?? '0, 0';
511
+ const color = attrs['color'] ?? '1, 1, 1, 1';
512
+ const thickness = attrs['thickness'] ?? '1.0';
513
+ body.push({ kind: 'draw_line', p1, p2, color, thickness, loc });
514
+ break;
515
+ }
516
+ case 'DrawRect': {
517
+ const min = attrs['min'] ?? '0, 0';
518
+ const max = attrs['max'] ?? '0, 0';
519
+ const color = attrs['color'] ?? '1, 1, 1, 1';
520
+ const filled = attrs['filled'] ?? 'false';
521
+ const thickness = attrs['thickness'] ?? '1.0';
522
+ const rounding = attrs['rounding'] ?? '0.0';
523
+ body.push({ kind: 'draw_rect', min, max, color, filled, thickness, rounding, loc });
524
+ break;
525
+ }
526
+ case 'DrawCircle': {
527
+ const center = attrs['center'] ?? '0, 0';
528
+ const radius = attrs['radius'] ?? '0';
529
+ const color = attrs['color'] ?? '1, 1, 1, 1';
530
+ const filled = attrs['filled'] ?? 'false';
531
+ const thickness = attrs['thickness'] ?? '1.0';
532
+ body.push({ kind: 'draw_circle', center, radius, color, filled, thickness, loc });
533
+ break;
534
+ }
535
+ case 'DrawText': {
536
+ const pos = attrs['pos'] ?? '0, 0';
537
+ const text = attrs['text'] ?? '""';
538
+ const color = attrs['color'] ?? '1, 1, 1, 1';
539
+ body.push({ kind: 'draw_text', pos, text, color, loc });
540
+ break;
541
+ }
402
542
  default:
403
543
  // Container self-closing (e.g., <Window title="X"/>)
404
544
  if (HOST_COMPONENTS[name]?.isContainer) {
405
545
  const containerTag = name;
406
- body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
546
+ body.push({ kind: 'begin_container', tag: containerTag, props: attrs, loc });
407
547
  body.push({ kind: 'end_container', tag: containerTag });
408
548
  }
409
549
  break;
410
550
  }
411
551
  }
412
- function lowerButton(attrs, rawAttrs, body, ctx) {
552
+ function lowerButton(attrs, rawAttrs, body, ctx, loc) {
413
553
  const title = attrs['title'] ?? '""';
414
554
  const onPressExpr = rawAttrs.get('onPress');
415
555
  let action = [];
@@ -417,9 +557,9 @@ function lowerButton(attrs, rawAttrs, body, ctx) {
417
557
  action = extractActionStatements(onPressExpr, ctx);
418
558
  }
419
559
  const style = attrs['style'];
420
- body.push({ kind: 'button', title, action, style });
560
+ body.push({ kind: 'button', title, action, style, loc });
421
561
  }
422
- function lowerTextInput(attrs, rawAttrs, body, ctx) {
562
+ function lowerTextInput(attrs, rawAttrs, body, ctx, loc) {
423
563
  const label = attrs['label'] ?? '""';
424
564
  const bufferIndex = ctx.bufferIndex++;
425
565
  // Detect bound state variable from value prop
@@ -432,9 +572,9 @@ function lowerTextInput(attrs, rawAttrs, body, ctx) {
432
572
  }
433
573
  }
434
574
  const style = attrs['style'];
435
- body.push({ kind: 'text_input', label, bufferIndex, stateVar, style });
575
+ body.push({ kind: 'text_input', label, bufferIndex, stateVar, style, loc });
436
576
  }
437
- function lowerCheckbox(attrs, rawAttrs, body, ctx) {
577
+ function lowerCheckbox(attrs, rawAttrs, body, ctx, loc) {
438
578
  const label = attrs['label'] ?? '""';
439
579
  // Detect bound state variable from value prop
440
580
  let stateVar = '';
@@ -467,9 +607,9 @@ function lowerCheckbox(attrs, rawAttrs, body, ctx) {
467
607
  }
468
608
  }
469
609
  const style = attrs['style'];
470
- body.push({ kind: 'checkbox', label, stateVar, valueExpr: valueExprStr, onChangeExpr: onChangeExprStr, style });
610
+ body.push({ kind: 'checkbox', label, stateVar, valueExpr: valueExprStr, onChangeExpr: onChangeExprStr, style, loc });
471
611
  }
472
- function lowerMenuItem(attrs, rawAttrs, body, ctx) {
612
+ function lowerMenuItem(attrs, rawAttrs, body, ctx, loc) {
473
613
  const label = attrs['label'] ?? '""';
474
614
  const shortcut = attrs['shortcut'];
475
615
  const onPressExpr = rawAttrs.get('onPress');
@@ -477,9 +617,9 @@ function lowerMenuItem(attrs, rawAttrs, body, ctx) {
477
617
  if (onPressExpr) {
478
618
  action = extractActionStatements(onPressExpr, ctx);
479
619
  }
480
- body.push({ kind: 'menu_item', label, shortcut, action });
620
+ body.push({ kind: 'menu_item', label, shortcut, action, loc });
481
621
  }
482
- function lowerTextElement(node, body, ctx) {
622
+ function lowerTextElement(node, body, ctx, loc) {
483
623
  let format = '';
484
624
  const args = [];
485
625
  for (const child of node.children) {
@@ -531,7 +671,7 @@ function lowerTextElement(node, body, ctx) {
531
671
  }
532
672
  }
533
673
  }
534
- body.push({ kind: 'text', format, args });
674
+ body.push({ kind: 'text', format, args, loc });
535
675
  }
536
676
  /**
537
677
  * Check if an expression will produce a const char* in C++ (not std::string).
@@ -593,7 +733,7 @@ function inferExprType(expr, ctx) {
593
733
  }
594
734
  return 'int'; // default
595
735
  }
596
- function lowerListMap(node, body, ctx) {
736
+ function lowerListMap(node, body, ctx, loc) {
597
737
  const propAccess = node.expression;
598
738
  const array = exprToCpp(propAccess.expression, ctx);
599
739
  const callback = node.arguments[0];
@@ -622,9 +762,10 @@ function lowerListMap(node, body, ctx) {
622
762
  stateCount: 0,
623
763
  bufferCount: 0,
624
764
  body: mapBody,
765
+ loc,
625
766
  });
626
767
  }
627
- function lowerCustomComponent(name, attributes, body, ctx) {
768
+ function lowerCustomComponent(name, attributes, body, ctx, loc) {
628
769
  const attrs = getAttributes(attributes, ctx);
629
770
  body.push({
630
771
  kind: 'custom_component',
@@ -632,8 +773,111 @@ function lowerCustomComponent(name, attributes, body, ctx) {
632
773
  props: attrs,
633
774
  stateCount: 0,
634
775
  bufferCount: 0,
776
+ loc,
635
777
  });
636
778
  }
779
+ function lowerNativeWidget(name, attributes, body, ctx, loc) {
780
+ const props = {};
781
+ const callbackProps = {};
782
+ const rawAttrs = getRawAttributes(attributes);
783
+ for (const [attrName, expr] of rawAttrs) {
784
+ if (attrName === 'key')
785
+ continue;
786
+ if (!expr) {
787
+ props[attrName] = 'true';
788
+ continue;
789
+ }
790
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
791
+ const params = expr.parameters;
792
+ if (params.length > 0) {
793
+ // Parameterized callback: (v: number) => setVol(v)
794
+ const param = params[0];
795
+ const paramName = ts.isIdentifier(param.name) ? param.name.text : '_p';
796
+ let cppType = 'float';
797
+ if (param.type) {
798
+ const typeText = param.type.getText();
799
+ if (typeText === 'number')
800
+ cppType = 'float';
801
+ else if (typeText === 'boolean')
802
+ cppType = 'bool';
803
+ else if (typeText === 'string')
804
+ cppType = 'std::string';
805
+ }
806
+ const bodyCode = ts.isBlock(expr.body)
807
+ ? expr.body.statements.map(s => stmtToCpp(s, ctx)).join(' ')
808
+ : exprToCpp(expr.body, ctx) + ';';
809
+ callbackProps[attrName] = `[&](std::any _v) { auto ${paramName} = std::any_cast<${cppType}>(_v); ${bodyCode} }`;
810
+ }
811
+ else {
812
+ // Void callback: () => doSomething()
813
+ const bodyCode = ts.isBlock(expr.body)
814
+ ? expr.body.statements.map(s => stmtToCpp(s, ctx)).join(' ')
815
+ : exprToCpp(expr.body, ctx) + ';';
816
+ callbackProps[attrName] = `[&](std::any) { ${bodyCode} }`;
817
+ }
818
+ }
819
+ else {
820
+ props[attrName] = exprToCpp(expr, ctx);
821
+ }
822
+ }
823
+ const keyAttr = rawAttrs.get('key');
824
+ const key = keyAttr ? exprToCpp(keyAttr, ctx) : undefined;
825
+ body.push({
826
+ kind: 'native_widget',
827
+ name,
828
+ props,
829
+ callbackProps,
830
+ key,
831
+ loc,
832
+ });
833
+ }
834
+ function lowerDockLayout(node, ctx) {
835
+ const children = [];
836
+ for (const child of node.children) {
837
+ if (ts.isJsxElement(child)) {
838
+ const tag = child.openingElement.tagName;
839
+ if (ts.isIdentifier(tag)) {
840
+ if (tag.text === 'DockSplit')
841
+ children.push(lowerDockSplit(child, ctx));
842
+ else if (tag.text === 'DockPanel')
843
+ children.push(lowerDockPanel(child, ctx));
844
+ }
845
+ }
846
+ }
847
+ return { kind: 'dock_layout', children, loc: getLoc(node, ctx) };
848
+ }
849
+ function lowerDockSplit(node, ctx) {
850
+ const attrs = getAttributes(node.openingElement.attributes, ctx);
851
+ const direction = attrs['direction'] ?? '"horizontal"';
852
+ const size = attrs['size'] ?? '0.5';
853
+ const children = [];
854
+ for (const child of node.children) {
855
+ if (ts.isJsxElement(child)) {
856
+ const tag = child.openingElement.tagName;
857
+ if (ts.isIdentifier(tag)) {
858
+ if (tag.text === 'DockSplit')
859
+ children.push(lowerDockSplit(child, ctx));
860
+ else if (tag.text === 'DockPanel')
861
+ children.push(lowerDockPanel(child, ctx));
862
+ }
863
+ }
864
+ }
865
+ return { kind: 'dock_split', direction, size, children };
866
+ }
867
+ function lowerDockPanel(node, ctx) {
868
+ const windows = [];
869
+ for (const child of node.children) {
870
+ if (ts.isJsxSelfClosingElement(child)) {
871
+ const tag = child.tagName;
872
+ if (ts.isIdentifier(tag) && tag.text === 'Window') {
873
+ const attrs = getAttributes(child.attributes, ctx);
874
+ if (attrs['title'])
875
+ windows.push(attrs['title']);
876
+ }
877
+ }
878
+ }
879
+ return { kind: 'dock_panel', windows };
880
+ }
637
881
  function lowerJsxChild(child, body, ctx) {
638
882
  if (ts.isJsxElement(child)) {
639
883
  lowerJsxElement(child, body, ctx);
@@ -650,6 +894,99 @@ function lowerJsxChild(child, body, ctx) {
650
894
  // Standalone text not inside <Text> — usually whitespace, skip
651
895
  }
652
896
  }
897
+ function lowerBulletTextElement(node, body, ctx, loc) {
898
+ // Same logic as lowerTextElement but produces bullet_text kind
899
+ const children = node.children;
900
+ const parts = [];
901
+ const args = [];
902
+ for (const child of children) {
903
+ if (ts.isJsxText(child)) {
904
+ const trimmed = child.text.trim();
905
+ if (trimmed)
906
+ parts.push(trimmed.replace(/%/g, '%%'));
907
+ }
908
+ else if (ts.isJsxExpression(child) && child.expression) {
909
+ args.push(exprToCpp(child.expression, ctx));
910
+ parts.push('%s');
911
+ }
912
+ }
913
+ const format = parts.join(' ');
914
+ body.push({ kind: 'bullet_text', format, args, loc });
915
+ }
916
+ function lowerLabelText(attrs, body, ctx, loc) {
917
+ const label = attrs['label'] ?? '""';
918
+ const value = attrs['value'] ?? '""';
919
+ body.push({ kind: 'label_text', label, value, loc });
920
+ }
921
+ function lowerSelectable(attrs, rawAttrs, body, ctx, loc) {
922
+ const label = attrs['label'] ?? '""';
923
+ const selected = attrs['selected'] ?? 'false';
924
+ const onSelectExpr = rawAttrs.get('onSelect');
925
+ let action = [];
926
+ if (onSelectExpr) {
927
+ action = extractActionStatements(onSelectExpr, ctx);
928
+ }
929
+ const style = attrs['style'];
930
+ body.push({ kind: 'selectable', label, selected, action, style, loc });
931
+ }
932
+ function lowerRadio(attrs, rawAttrs, body, ctx, loc) {
933
+ const label = attrs['label'] ?? '""';
934
+ const index = attrs['index'] ?? '0';
935
+ const style = attrs['style'];
936
+ const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
937
+ body.push({ kind: 'radio', label, stateVar, valueExpr, onChangeExpr, index, style, loc });
938
+ }
939
+ function lowerInputTextMultiline(attrs, rawAttrs, body, ctx, loc) {
940
+ const label = attrs['label'] ?? '""';
941
+ const bufferIndex = ctx.bufferIndex++;
942
+ let stateVar = '';
943
+ const valueExpr = rawAttrs.get('value');
944
+ if (valueExpr && ts.isIdentifier(valueExpr)) {
945
+ const varName = valueExpr.text;
946
+ if (ctx.stateVars.has(varName)) {
947
+ stateVar = varName;
948
+ }
949
+ }
950
+ const style = attrs['style'];
951
+ body.push({ kind: 'input_text_multiline', label, bufferIndex, stateVar, style, loc });
952
+ }
953
+ function lowerColorPicker(attrs, rawAttrs, body, ctx, loc) {
954
+ const label = attrs['label'] ?? '""';
955
+ const style = attrs['style'];
956
+ let stateVar = '';
957
+ const valueRaw = rawAttrs.get('value');
958
+ if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
959
+ stateVar = valueRaw.text;
960
+ }
961
+ body.push({ kind: 'color_picker', label, stateVar, style, loc });
962
+ }
963
+ function lowerPlotLines(attrs, body, ctx, loc) {
964
+ const label = attrs['label'] ?? '""';
965
+ const values = attrs['values'] ?? '';
966
+ const overlay = attrs['overlay'];
967
+ const style = attrs['style'];
968
+ body.push({ kind: 'plot_lines', label, values, overlay, style, loc });
969
+ }
970
+ function lowerPlotHistogram(attrs, body, ctx, loc) {
971
+ const label = attrs['label'] ?? '""';
972
+ const values = attrs['values'] ?? '';
973
+ const overlay = attrs['overlay'];
974
+ const style = attrs['style'];
975
+ body.push({ kind: 'plot_histogram', label, values, overlay, style, loc });
976
+ }
977
+ function lowerImage(attrs, body, ctx, loc) {
978
+ const src = attrs['src'] ?? '""';
979
+ const embed = attrs['embed'] === 'true';
980
+ const width = attrs['width'];
981
+ const height = attrs['height'];
982
+ let embedKey;
983
+ if (embed) {
984
+ // Derive key from src: strip quotes, replace non-alnum with underscore
985
+ const rawSrc = src.replace(/^"|"$/g, '');
986
+ embedKey = rawSrc.replace(/[^a-zA-Z0-9]/g, '_');
987
+ }
988
+ body.push({ kind: 'image', src, embed, embedKey, width, height, loc });
989
+ }
653
990
  function getAttributes(attributes, ctx) {
654
991
  const result = {};
655
992
  for (const attr of attributes.properties) {
@@ -709,56 +1046,56 @@ function lowerValueOnChange(rawAttrs, ctx) {
709
1046
  }
710
1047
  return { stateVar, valueExpr, onChangeExpr };
711
1048
  }
712
- function lowerSliderFloat(attrs, rawAttrs, body, ctx) {
1049
+ function lowerSliderFloat(attrs, rawAttrs, body, ctx, loc) {
713
1050
  const label = attrs['label'] ?? '""';
714
1051
  const min = attrs['min'] ?? '0.0f';
715
1052
  const max = attrs['max'] ?? '1.0f';
716
1053
  const style = attrs['style'];
717
1054
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
718
- body.push({ kind: 'slider_float', label, stateVar, valueExpr, onChangeExpr, min, max, style });
1055
+ body.push({ kind: 'slider_float', label, stateVar, valueExpr, onChangeExpr, min, max, style, loc });
719
1056
  }
720
- function lowerSliderInt(attrs, rawAttrs, body, ctx) {
1057
+ function lowerSliderInt(attrs, rawAttrs, body, ctx, loc) {
721
1058
  const label = attrs['label'] ?? '""';
722
1059
  const min = attrs['min'] ?? '0';
723
1060
  const max = attrs['max'] ?? '100';
724
1061
  const style = attrs['style'];
725
1062
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
726
- body.push({ kind: 'slider_int', label, stateVar, valueExpr, onChangeExpr, min, max, style });
1063
+ body.push({ kind: 'slider_int', label, stateVar, valueExpr, onChangeExpr, min, max, style, loc });
727
1064
  }
728
- function lowerDragFloat(attrs, rawAttrs, body, ctx) {
1065
+ function lowerDragFloat(attrs, rawAttrs, body, ctx, loc) {
729
1066
  const label = attrs['label'] ?? '""';
730
1067
  const speed = attrs['speed'] ?? '1.0f';
731
1068
  const style = attrs['style'];
732
1069
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
733
- body.push({ kind: 'drag_float', label, stateVar, valueExpr, onChangeExpr, speed, style });
1070
+ body.push({ kind: 'drag_float', label, stateVar, valueExpr, onChangeExpr, speed, style, loc });
734
1071
  }
735
- function lowerDragInt(attrs, rawAttrs, body, ctx) {
1072
+ function lowerDragInt(attrs, rawAttrs, body, ctx, loc) {
736
1073
  const label = attrs['label'] ?? '""';
737
1074
  const speed = attrs['speed'] ?? '1.0f';
738
1075
  const style = attrs['style'];
739
1076
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
740
- body.push({ kind: 'drag_int', label, stateVar, valueExpr, onChangeExpr, speed, style });
1077
+ body.push({ kind: 'drag_int', label, stateVar, valueExpr, onChangeExpr, speed, style, loc });
741
1078
  }
742
- function lowerCombo(attrs, rawAttrs, body, ctx) {
1079
+ function lowerCombo(attrs, rawAttrs, body, ctx, loc) {
743
1080
  const label = attrs['label'] ?? '""';
744
1081
  const items = attrs['items'] ?? '';
745
1082
  const style = attrs['style'];
746
1083
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
747
- body.push({ kind: 'combo', label, stateVar, valueExpr, onChangeExpr, items, style });
1084
+ body.push({ kind: 'combo', label, stateVar, valueExpr, onChangeExpr, items, style, loc });
748
1085
  }
749
- function lowerInputInt(attrs, rawAttrs, body, ctx) {
1086
+ function lowerInputInt(attrs, rawAttrs, body, ctx, loc) {
750
1087
  const label = attrs['label'] ?? '""';
751
1088
  const style = attrs['style'];
752
1089
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
753
- body.push({ kind: 'input_int', label, stateVar, valueExpr, onChangeExpr, style });
1090
+ body.push({ kind: 'input_int', label, stateVar, valueExpr, onChangeExpr, style, loc });
754
1091
  }
755
- function lowerInputFloat(attrs, rawAttrs, body, ctx) {
1092
+ function lowerInputFloat(attrs, rawAttrs, body, ctx, loc) {
756
1093
  const label = attrs['label'] ?? '""';
757
1094
  const style = attrs['style'];
758
1095
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
759
- body.push({ kind: 'input_float', label, stateVar, valueExpr, onChangeExpr, style });
1096
+ body.push({ kind: 'input_float', label, stateVar, valueExpr, onChangeExpr, style, loc });
760
1097
  }
761
- function lowerColorEdit(attrs, rawAttrs, body, ctx) {
1098
+ function lowerColorEdit(attrs, rawAttrs, body, ctx, loc) {
762
1099
  const label = attrs['label'] ?? '""';
763
1100
  const style = attrs['style'];
764
1101
  // ColorEdit only supports state-bound values
@@ -767,22 +1104,22 @@ function lowerColorEdit(attrs, rawAttrs, body, ctx) {
767
1104
  if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
768
1105
  stateVar = valueRaw.text;
769
1106
  }
770
- body.push({ kind: 'color_edit', label, stateVar, style });
1107
+ body.push({ kind: 'color_edit', label, stateVar, style, loc });
771
1108
  }
772
- function lowerListBox(attrs, rawAttrs, body, ctx) {
1109
+ function lowerListBox(attrs, rawAttrs, body, ctx, loc) {
773
1110
  const label = attrs['label'] ?? '""';
774
1111
  const items = attrs['items'] ?? '';
775
1112
  const style = attrs['style'];
776
1113
  const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
777
- body.push({ kind: 'list_box', label, stateVar, valueExpr, onChangeExpr, items, style });
1114
+ body.push({ kind: 'list_box', label, stateVar, valueExpr, onChangeExpr, items, style, loc });
778
1115
  }
779
- function lowerProgressBar(attrs, rawAttrs, body, ctx) {
1116
+ function lowerProgressBar(attrs, rawAttrs, body, ctx, loc) {
780
1117
  const value = attrs['value'] ?? '0.0f';
781
1118
  const overlay = attrs['overlay'];
782
1119
  const style = attrs['style'];
783
- body.push({ kind: 'progress_bar', value, overlay, style });
1120
+ body.push({ kind: 'progress_bar', value, overlay, style, loc });
784
1121
  }
785
- function lowerTooltip(attrs, body, ctx) {
1122
+ function lowerTooltip(attrs, body, ctx, loc) {
786
1123
  const text = attrs['text'] ?? '""';
787
- body.push({ kind: 'tooltip', text });
1124
+ body.push({ kind: 'tooltip', text, loc });
788
1125
  }