1pub fn emit_widget_types_ts() -> String {
8 let mut out = String::new();
9 out.push_str(
10 "/**\n\
11 * Generated widget IR types — do not edit by hand.\n\
12 * Source of truth: Rust `src/models.rs` via `cargo run --bin gen-ts --features codegen`.\n\
13 * Emitter: `src/codegen.rs` IR_SPEC.\n\
14 */\n\n",
15 );
16
17 out.push_str(
18 "// ── Enums ──\n\n\
19 export type FontWeight = \"ultralight\" | \"thin\" | \"light\" | \"regular\" | \"medium\" | \"semibold\" | \"bold\" | \"heavy\" | \"black\";\n\
20 export type FontDesign = \"default\" | \"monospaced\" | \"rounded\" | \"serif\";\n\
21 export type TextAlignment = \"leading\" | \"center\" | \"trailing\";\n\
22 export type HorizontalAlignment = \"leading\" | \"center\" | \"trailing\";\n\
23 export type VerticalAlignment = \"top\" | \"center\" | \"bottom\";\n\
24 export type ContentMode = \"fit\" | \"fill\";\n\
25 export type ProgressStyle = \"linear\" | \"circular\";\n\
26 export type GaugeStyle = \"circular\" | \"linear\";\n\
27 export type DateStyle = \"time\" | \"date\" | \"relative\" | \"offset\" | \"timer\";\n\
28 export type ChartType = \"bar\" | \"line\" | \"area\" | \"pie\";\n\
29 export type ShapeType = \"circle\" | \"capsule\" | \"rectangle\";\n\
30 export type TimerCounting = \"up\" | \"down\";\n\
31 export type ClipShape = \"circle\" | \"capsule\" | \"rectangle\";\n\
32 export type TextStyle =\n\
33 | \"largeTitle\" | \"title\" | \"title2\" | \"title3\"\n\
34 | \"headline\" | \"subheadline\"\n\
35 | \"body\" | \"callout\"\n\
36 | \"footnote\" | \"caption\" | \"caption2\";\n\
37 export type GradientType = \"linear\" | \"radial\" | \"angular\";\n\
38 export type GradientDirection =\n\
39 | \"topToBottom\" | \"bottomToTop\"\n\
40 | \"leadingToTrailing\" | \"trailingToLeading\"\n\
41 | \"topLeadingToBottomTrailing\" | \"topTrailingToBottomLeading\";\n\n",
42 );
43
44 out.push_str(
45 "// ── Supporting types ──\n\n\
46 export type ColorValue = string | { light: string; dark: string };\n\n\
47 export interface ChartDataPoint {\n\
48 label: string;\n\
49 value: number;\n\
50 color?: ColorValue;\n\
51 }\n\n\
52 export interface FrameConfig {\n\
53 width?: number;\n\
54 height?: number;\n\
55 maxWidth?: number | \"infinity\";\n\
56 maxHeight?: number | \"infinity\";\n\
57 }\n\n\
58 export interface BorderConfig {\n\
59 color: string;\n\
60 width?: number;\n\
61 }\n\n\
62 export interface GradientConfig {\n\
63 gradientType: GradientType;\n\
64 colors: string[];\n\
65 direction?: GradientDirection;\n\
66 }\n\n\
67 export interface ShadowConfig {\n\
68 color?: string;\n\
69 radius?: number;\n\
70 x?: number;\n\
71 y?: number;\n\
72 }\n\n\
73 export type BackgroundValue = string | GradientConfig | { light: string; dark: string };\n\n\
74 export type PaddingValue = number | {\n\
75 top?: number;\n\
76 bottom?: number;\n\
77 leading?: number;\n\
78 trailing?: number;\n\
79 };\n\n\
80 export interface ElementStyle {\n\
81 padding?: PaddingValue;\n\
82 background?: BackgroundValue;\n\
83 cornerRadius?: number;\n\
84 opacity?: number;\n\
85 frame?: FrameConfig;\n\
86 border?: BorderConfig;\n\
87 shadow?: ShadowConfig;\n\
88 clipShape?: ClipShape;\n\
89 flex?: number;\n\
90 }\n\n",
91 );
92
93 for spec in IR_ELEMENTS {
95 out.push_str(&format!(
96 "export interface {} extends ElementStyle {{\n type: \"{}\";\n",
97 spec.ts_name, spec.wire
98 ));
99 for field in spec.fields {
100 out.push_str(&format!(" {};\n", field));
101 }
102 out.push_str("}\n\n");
103 }
104
105 out.push_str(
106 "export interface SpacerElement {\n\
107 type: \"spacer\";\n\
108 minLength?: number;\n\
109 }\n\n\
110 export interface ListItem {\n\
111 text: string;\n\
112 checked?: boolean;\n\
113 action?: string;\n\
114 payload?: string;\n\
115 }\n\n\
116 export interface CanvasCircle {\n\
117 draw: \"circle\";\n\
118 cx: number; cy: number; r: number;\n\
119 fill?: ColorValue; stroke?: ColorValue; strokeWidth?: number;\n\
120 }\n\
121 export interface CanvasLine {\n\
122 draw: \"line\";\n\
123 x1: number; y1: number; x2: number; y2: number;\n\
124 stroke?: ColorValue; strokeWidth?: number; lineCap?: \"butt\" | \"round\" | \"square\";\n\
125 }\n\
126 export interface CanvasRect {\n\
127 draw: \"rect\";\n\
128 x: number; y: number; width: number; height: number;\n\
129 fill?: ColorValue; stroke?: ColorValue; strokeWidth?: number; cornerRadius?: number;\n\
130 }\n\
131 export interface CanvasArc {\n\
132 draw: \"arc\";\n\
133 cx: number; cy: number; r: number;\n\
134 startAngle: number; endAngle: number;\n\
135 fill?: ColorValue; stroke?: ColorValue; strokeWidth?: number;\n\
136 }\n\
137 export interface CanvasText {\n\
138 draw: \"text\";\n\
139 x: number; y: number; content: string;\n\
140 fontSize?: number; color?: ColorValue; anchor?: \"start\" | \"middle\" | \"end\";\n\
141 }\n\
142 export interface CanvasPath {\n\
143 draw: \"path\";\n\
144 d: string;\n\
145 fill?: ColorValue; stroke?: ColorValue; strokeWidth?: number;\n\
146 }\n\
147 export type CanvasDrawCommand = CanvasCircle | CanvasLine | CanvasRect | CanvasArc | CanvasText | CanvasPath;\n\n",
148 );
149
150 out.push_str("export type WidgetElement =\n");
152 let names: Vec<&str> = IR_ELEMENTS
153 .iter()
154 .map(|e| e.ts_name)
155 .chain(std::iter::once("SpacerElement"))
156 .collect();
157 for (i, n) in names.iter().enumerate() {
158 let sep = if i + 1 == names.len() { ";" } else { "" };
159 out.push_str(&format!(" | {}{}\n", n, sep));
160 }
161 out.push('\n');
162
163 out.push_str(
164 "export interface WidgetConfig {\n\
165 version?: number;\n\
166 small?: WidgetElement;\n\
167 medium?: WidgetElement;\n\
168 large?: WidgetElement;\n\
169 }\n",
170 );
171
172 out
173}
174
175struct ElementSpec {
176 wire: &'static str,
177 ts_name: &'static str,
178 fields: &'static [&'static str],
179}
180
181const IR_ELEMENTS: &[ElementSpec] = &[
184 ElementSpec {
185 wire: "vstack",
186 ts_name: "VStackElement",
187 fields: &[
188 "children: WidgetElement[]",
189 "spacing?: number",
190 "alignment?: HorizontalAlignment",
191 ],
192 },
193 ElementSpec {
194 wire: "hstack",
195 ts_name: "HStackElement",
196 fields: &[
197 "children: WidgetElement[]",
198 "spacing?: number",
199 "alignment?: VerticalAlignment",
200 ],
201 },
202 ElementSpec {
203 wire: "zstack",
204 ts_name: "ZStackElement",
205 fields: &["children: WidgetElement[]", "alignment?: string"],
206 },
207 ElementSpec {
208 wire: "grid",
209 ts_name: "GridElement",
210 fields: &[
211 "children: WidgetElement[]",
212 "columns?: number",
213 "spacing?: number",
214 "rowSpacing?: number",
215 ],
216 },
217 ElementSpec {
218 wire: "container",
219 ts_name: "ContainerElement",
220 fields: &["children?: WidgetElement[]", "contentAlignment?: string"],
221 },
222 ElementSpec {
223 wire: "text",
224 ts_name: "TextElement",
225 fields: &[
226 "content: string",
227 "fontSize?: number",
228 "fontWeight?: FontWeight",
229 "fontDesign?: FontDesign",
230 "textStyle?: TextStyle",
231 "color?: ColorValue",
232 "alignment?: TextAlignment",
233 "lineLimit?: number",
234 ],
235 },
236 ElementSpec {
237 wire: "image",
238 ts_name: "ImageElement",
239 fields: &[
240 "systemName?: string",
241 "data?: string",
242 "url?: string",
243 "size?: number",
244 "color?: ColorValue",
245 "contentMode?: ContentMode",
246 ],
247 },
248 ElementSpec {
249 wire: "progress",
250 ts_name: "ProgressElement",
251 fields: &[
252 "value: number",
253 "total?: number",
254 "label?: string",
255 "tint?: ColorValue",
256 "color?: ColorValue",
257 "barStyle?: ProgressStyle",
258 ],
259 },
260 ElementSpec {
261 wire: "gauge",
262 ts_name: "GaugeElement",
263 fields: &[
264 "value: number",
265 "min?: number",
266 "max?: number",
267 "label?: string",
268 "currentValueLabel?: string",
269 "tint?: ColorValue",
270 "color?: ColorValue",
271 "gaugeStyle?: GaugeStyle",
272 ],
273 },
274 ElementSpec {
275 wire: "button",
276 ts_name: "ButtonElement",
277 fields: &[
278 "label: string",
279 "url?: string",
280 "action?: string",
281 "color?: ColorValue",
282 "backgroundColor?: ColorValue",
283 "fontSize?: number",
284 "textAlignment?: TextAlignment",
285 ],
286 },
287 ElementSpec {
288 wire: "toggle",
289 ts_name: "ToggleElement",
290 fields: &[
291 "isOn: boolean",
292 "label?: string",
293 "tint?: ColorValue",
294 "action?: string",
295 ],
296 },
297 ElementSpec {
298 wire: "divider",
299 ts_name: "DividerElement",
300 fields: &["color?: ColorValue", "thickness?: number"],
301 },
302 ElementSpec {
303 wire: "date",
304 ts_name: "DateElement",
305 fields: &[
306 "date: string",
307 "dateStyle?: DateStyle",
308 "fontSize?: number",
309 "color?: ColorValue",
310 ],
311 },
312 ElementSpec {
313 wire: "chart",
314 ts_name: "ChartElement",
315 fields: &[
316 "chartType: ChartType",
317 "chartData: ChartDataPoint[]",
318 "tint?: ColorValue",
319 ],
320 },
321 ElementSpec {
322 wire: "list",
323 ts_name: "ListElement",
324 fields: &[
325 "items: ListItem[]",
326 "spacing?: number",
327 "fontSize?: number",
328 "color?: ColorValue",
329 ],
330 },
331 ElementSpec {
332 wire: "link",
333 ts_name: "LinkElement",
334 fields: &[
335 "children: WidgetElement[]",
336 "url?: string",
337 "action?: string",
338 ],
339 },
340 ElementSpec {
341 wire: "shape",
342 ts_name: "ShapeElement",
343 fields: &[
344 "shapeType: ShapeType",
345 "fill?: ColorValue",
346 "stroke?: ColorValue",
347 "strokeWidth?: number",
348 "size?: number",
349 ],
350 },
351 ElementSpec {
352 wire: "timer",
353 ts_name: "TimerElement",
354 fields: &[
355 "targetDate: string",
356 "counting?: TimerCounting",
357 "fontSize?: number",
358 "fontWeight?: FontWeight",
359 "color?: ColorValue",
360 ],
361 },
362 ElementSpec {
363 wire: "canvas",
364 ts_name: "CanvasElement",
365 fields: &[
366 "width: number",
367 "height: number",
368 "elements: CanvasDrawCommand[]",
369 ],
370 },
371 ElementSpec {
372 wire: "label",
373 ts_name: "LabelElement",
374 fields: &[
375 "text: string",
376 "systemName: string",
377 "iconColor?: ColorValue",
378 "fontSize?: number",
379 "fontWeight?: FontWeight",
380 "color?: ColorValue",
381 "spacing?: number",
382 ],
383 },
384];
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::capabilities::ELEMENT_TYPES;
390
391 #[test]
392 fn ir_spec_covers_all_element_types() {
393 let wires: Vec<&str> = IR_ELEMENTS
394 .iter()
395 .map(|e| e.wire)
396 .chain(["spacer"])
397 .collect();
398 for ty in ELEMENT_TYPES {
399 assert!(wires.contains(ty), "IR_ELEMENTS missing wire type `{ty}`");
400 }
401 assert_eq!(wires.len(), ELEMENT_TYPES.len());
402 }
403
404 #[test]
405 fn codegen_covers_all_element_types() {
406 let ts = emit_widget_types_ts();
407 for ty in ELEMENT_TYPES {
408 let needle = format!("type: \"{ty}\"");
409 assert!(
410 ts.contains(&needle),
411 "generated TS missing element type `{ty}`"
412 );
413 }
414 assert!(ts.contains("export interface WidgetConfig"));
415 assert!(ts.contains("export type WidgetElement"));
416 assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
418 }
419
420 #[test]
421 fn type_name_exhaustive() {
422 use crate::models::{SpacerElement, WidgetElement};
424 fn check(el: &WidgetElement) -> &'static str {
425 el.type_name()
426 }
427 let _ = check(&WidgetElement::Spacer(SpacerElement { min_length: None }));
428 }
429}