Skip to main content

tauri_plugin_widgets/
codegen.rs

1//! Emit TypeScript IR types from an explicit Rust `IR_ELEMENTS` spec (SoT with models).
2//!
3//! Static preamble / enums / supporting / footer live in `src/codegen/*.ts` via
4//! `include_str!`. The remaining `push_str` calls are intentional: per-element
5//! interfaces and the `WidgetElement` union are generated from `IR_ELEMENTS`
6//! so exhaustiveness tests stay coupled to [`crate::models::WidgetElement`].
7
8/// Emit `guest-js/generated/widget-types.ts`.
9pub fn emit_widget_types_ts() -> String {
10    let mut out = String::new();
11    out.push_str(include_str!("codegen/preamble.ts"));
12    out.push('\n');
13    out.push_str(include_str!("codegen/enums.ts"));
14    out.push_str(include_str!("codegen/supporting.ts"));
15
16    // Element interfaces — IR_SPEC mapping (must stay in sync with WidgetElement)
17    for spec in IR_ELEMENTS {
18        out.push_str(&format!(
19            "export interface {} extends ElementStyle {{\n  type: \"{}\";\n",
20            spec.ts_name, spec.wire
21        ));
22        for field in spec.fields {
23            out.push_str(&format!("  {};\n", field));
24        }
25        out.push_str("}\n\n");
26    }
27
28    out.push_str("export type WidgetElement =\n");
29    let names: Vec<&str> = IR_ELEMENTS
30        .iter()
31        .map(|e| e.ts_name)
32        .chain(std::iter::once("SpacerElement"))
33        .collect();
34    for (i, n) in names.iter().enumerate() {
35        let sep = if i + 1 == names.len() { ";" } else { "" };
36        out.push_str(&format!("  | {}{}\n", n, sep));
37    }
38    out.push('\n');
39
40    out.push_str(include_str!("codegen/footer.ts"));
41
42    out
43}
44
45struct ElementSpec {
46    wire: &'static str,
47    ts_name: &'static str,
48    fields: &'static [&'static str],
49}
50
51/// Explicit IR → TS mapping. When adding a [`WidgetElement`] variant, extend this list
52/// and the exhaustive match in tests.
53const IR_ELEMENTS: &[ElementSpec] = &[
54    ElementSpec {
55        wire: "vstack",
56        ts_name: "VStackElement",
57        fields: &[
58            "children: WidgetElement[]",
59            "spacing?: number",
60            "alignment?: HorizontalAlignment",
61        ],
62    },
63    ElementSpec {
64        wire: "hstack",
65        ts_name: "HStackElement",
66        fields: &[
67            "children: WidgetElement[]",
68            "spacing?: number",
69            "alignment?: VerticalAlignment",
70        ],
71    },
72    ElementSpec {
73        wire: "zstack",
74        ts_name: "ZStackElement",
75        fields: &["children: WidgetElement[]", "alignment?: string"],
76    },
77    ElementSpec {
78        wire: "grid",
79        ts_name: "GridElement",
80        fields: &[
81            "children: WidgetElement[]",
82            "columns?: number",
83            "spacing?: number",
84            "rowSpacing?: number",
85        ],
86    },
87    ElementSpec {
88        wire: "container",
89        ts_name: "ContainerElement",
90        fields: &["children?: WidgetElement[]", "contentAlignment?: string"],
91    },
92    ElementSpec {
93        wire: "text",
94        ts_name: "TextElement",
95        fields: &[
96            "content: string",
97            "fontSize?: number",
98            "fontWeight?: FontWeight",
99            "fontDesign?: FontDesign",
100            "textStyle?: TextStyle",
101            "color?: ColorValue",
102            "alignment?: TextAlignment",
103            "lineLimit?: number",
104        ],
105    },
106    ElementSpec {
107        wire: "image",
108        ts_name: "ImageElement",
109        fields: &[
110            "systemName?: string",
111            "data?: string",
112            "url?: string",
113            "size?: number",
114            "color?: ColorValue",
115            "contentMode?: ContentMode",
116        ],
117    },
118    ElementSpec {
119        wire: "progress",
120        ts_name: "ProgressElement",
121        fields: &[
122            "value: number",
123            "total?: number",
124            "label?: string",
125            "tint?: ColorValue",
126            "color?: ColorValue",
127            "barStyle?: ProgressStyle",
128        ],
129    },
130    ElementSpec {
131        wire: "gauge",
132        ts_name: "GaugeElement",
133        fields: &[
134            "value: number",
135            "min?: number",
136            "max?: number",
137            "label?: string",
138            "currentValueLabel?: string",
139            "tint?: ColorValue",
140            "color?: ColorValue",
141            "gaugeStyle?: GaugeStyle",
142        ],
143    },
144    ElementSpec {
145        wire: "button",
146        ts_name: "ButtonElement",
147        fields: &[
148            "label: string",
149            "url?: string",
150            "action?: string",
151            "color?: ColorValue",
152            "backgroundColor?: ColorValue",
153            "fontSize?: number",
154            "textAlignment?: TextAlignment",
155        ],
156    },
157    ElementSpec {
158        wire: "toggle",
159        ts_name: "ToggleElement",
160        fields: &[
161            "isOn: boolean",
162            "label?: string",
163            "tint?: ColorValue",
164            "action?: string",
165        ],
166    },
167    ElementSpec {
168        wire: "divider",
169        ts_name: "DividerElement",
170        fields: &["color?: ColorValue", "thickness?: number"],
171    },
172    ElementSpec {
173        wire: "date",
174        ts_name: "DateElement",
175        fields: &[
176            "date: string",
177            "dateStyle?: DateStyle",
178            "fontSize?: number",
179            "color?: ColorValue",
180        ],
181    },
182    ElementSpec {
183        wire: "chart",
184        ts_name: "ChartElement",
185        fields: &[
186            "chartType: ChartType",
187            "chartData: ChartDataPoint[]",
188            "tint?: ColorValue",
189        ],
190    },
191    ElementSpec {
192        wire: "list",
193        ts_name: "ListElement",
194        fields: &[
195            "items: ListItem[]",
196            "spacing?: number",
197            "fontSize?: number",
198            "color?: ColorValue",
199        ],
200    },
201    ElementSpec {
202        wire: "link",
203        ts_name: "LinkElement",
204        fields: &[
205            "children: WidgetElement[]",
206            "url?: string",
207            "action?: string",
208        ],
209    },
210    ElementSpec {
211        wire: "shape",
212        ts_name: "ShapeElement",
213        fields: &[
214            "shapeType: ShapeType",
215            "fill?: ColorValue",
216            "stroke?: ColorValue",
217            "strokeWidth?: number",
218            "size?: number",
219        ],
220    },
221    ElementSpec {
222        wire: "timer",
223        ts_name: "TimerElement",
224        fields: &[
225            "targetDate: string",
226            "counting?: TimerCounting",
227            "fontSize?: number",
228            "fontWeight?: FontWeight",
229            "color?: ColorValue",
230        ],
231    },
232    ElementSpec {
233        wire: "canvas",
234        ts_name: "CanvasElement",
235        fields: &[
236            "width: number",
237            "height: number",
238            "elements: CanvasDrawCommand[]",
239        ],
240    },
241    ElementSpec {
242        wire: "label",
243        ts_name: "LabelElement",
244        fields: &[
245            "text: string",
246            "systemName: string",
247            "iconColor?: ColorValue",
248            "fontSize?: number",
249            "fontWeight?: FontWeight",
250            "color?: ColorValue",
251            "spacing?: number",
252        ],
253    },
254];
255
256/// Wire type strings + field hints for Swift (flat Codable stays in Models.swift).
257pub fn emit_wire_catalog_swift() -> String {
258    let mut out = String::from(
259        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
260         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
261         // Runtime decode still uses flat `WidgetElement` in Models.swift.\n\n\
262         import Foundation\n\n\
263         /// Known `type` wire values for widget IR elements.\n\
264         public enum WidgetWireType: String, CaseIterable, Sendable {\n",
265    );
266    for spec in IR_ELEMENTS {
267        let case = swift_case_name(spec.wire);
268        out.push_str(&format!("    case {case} = \"{}\"\n", spec.wire));
269    }
270    out.push_str("    case spacer = \"spacer\"\n}\n\n");
271    out.push_str("/// Per-element field hints (documentation / drift checks; not Codable).\n");
272    out.push_str("public enum WidgetWireFields {\n");
273    for spec in IR_ELEMENTS {
274        let case = swift_case_name(spec.wire);
275        let fields: Vec<String> = spec
276            .fields
277            .iter()
278            .map(|f| format!("\"{}\"", f.replace('\\', "\\\\").replace('"', "\\\"")))
279            .collect();
280        out.push_str(&format!(
281            "    public static let {case}: [String] = [{}]\n",
282            fields.join(", ")
283        ));
284    }
285    out.push_str("    public static let spacer: [String] = [\"minLength?: number\"]\n");
286    out.push_str("}\n");
287    out
288}
289
290/// Wire type constants for Android Glance renderer (JSONObject path stays dynamic).
291pub fn emit_wire_catalog_kotlin() -> String {
292    let mut out = String::from(
293        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
294         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
295         // Runtime still uses org.json.JSONObject via render.El.\n\n\
296         package git.s00d.widgets\n\n\
297         /** Known `type` wire values for widget IR elements. */\n\
298         object WireTypes {\n",
299    );
300    for spec in IR_ELEMENTS {
301        let const_name = kotlin_const_name(spec.wire);
302        out.push_str(&format!(
303            "    const val {const_name} = \"{}\"\n",
304            spec.wire
305        ));
306    }
307    out.push_str("    const val SPACER = \"spacer\"\n\n");
308    out.push_str("    val ALL: Set<String> = setOf(\n");
309    for spec in IR_ELEMENTS {
310        out.push_str(&format!("        {},\n", kotlin_const_name(spec.wire)));
311    }
312    out.push_str("        SPACER,\n    )\n}\n");
313    out
314}
315
316fn swift_case_name(wire: &str) -> String {
317    // camelCase identifiers from wire names (already lower).
318    wire.to_string()
319}
320
321fn kotlin_const_name(wire: &str) -> String {
322    wire.to_uppercase()
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::capabilities::ELEMENT_TYPES;
329
330    #[test]
331    fn ir_spec_covers_all_element_types() {
332        let wires: Vec<&str> = IR_ELEMENTS
333            .iter()
334            .map(|e| e.wire)
335            .chain(["spacer"])
336            .collect();
337        for ty in ELEMENT_TYPES {
338            assert!(wires.contains(ty), "IR_ELEMENTS missing wire type `{ty}`");
339        }
340        assert_eq!(wires.len(), ELEMENT_TYPES.len());
341    }
342
343    #[test]
344    fn codegen_covers_all_element_types() {
345        let ts = emit_widget_types_ts();
346        for ty in ELEMENT_TYPES {
347            let needle = format!("type: \"{ty}\"");
348            assert!(
349                ts.contains(&needle),
350                "generated TS missing element type `{ty}`"
351            );
352        }
353        assert!(ts.contains("export interface WidgetConfig"));
354        assert!(ts.contains("export type WidgetElement"));
355        // Must not be a stale include_str twin
356        assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
357    }
358
359    #[test]
360    fn swift_catalog_covers_all_element_types() {
361        let swift = emit_wire_catalog_swift();
362        for ty in ELEMENT_TYPES {
363            assert!(
364                swift.contains(&format!("= \"{ty}\"")),
365                "Swift catalog missing `{ty}`"
366            );
367        }
368    }
369
370    #[test]
371    fn kotlin_catalog_covers_all_element_types() {
372        let kt = emit_wire_catalog_kotlin();
373        for ty in ELEMENT_TYPES {
374            assert!(
375                kt.contains(&format!("= \"{ty}\"")),
376                "Kotlin catalog missing `{ty}`"
377            );
378        }
379    }
380
381    #[test]
382    fn type_name_exhaustive() {
383        // Compiling this match fails if a WidgetElement variant is added without update.
384        use crate::models::{SpacerElement, WidgetElement};
385        fn check(el: &WidgetElement) -> &'static str {
386            el.type_name()
387        }
388        let _ = check(&WidgetElement::Spacer(SpacerElement { min_length: None }));
389    }
390}