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            "children?: WidgetElement[]",
197            "spacing?: number",
198            "fontSize?: number",
199            "color?: ColorValue",
200        ],
201    },
202    ElementSpec {
203        wire: "link",
204        ts_name: "LinkElement",
205        fields: &[
206            "children: WidgetElement[]",
207            "url?: string",
208            "action?: string",
209        ],
210    },
211    ElementSpec {
212        wire: "shape",
213        ts_name: "ShapeElement",
214        fields: &[
215            "shapeType: ShapeType",
216            "fill?: ColorValue",
217            "stroke?: ColorValue",
218            "strokeWidth?: number",
219            "size?: number",
220        ],
221    },
222    ElementSpec {
223        wire: "timer",
224        ts_name: "TimerElement",
225        fields: &[
226            "targetDate: string",
227            "counting?: TimerCounting",
228            "fontSize?: number",
229            "fontWeight?: FontWeight",
230            "color?: ColorValue",
231        ],
232    },
233    ElementSpec {
234        wire: "canvas",
235        ts_name: "CanvasElement",
236        fields: &[
237            "width: number",
238            "height: number",
239            "elements: CanvasDrawCommand[]",
240        ],
241    },
242    ElementSpec {
243        wire: "label",
244        ts_name: "LabelElement",
245        fields: &[
246            "text: string",
247            "systemName: string",
248            "iconColor?: ColorValue",
249            "fontSize?: number",
250            "fontWeight?: FontWeight",
251            "color?: ColorValue",
252            "spacing?: number",
253        ],
254    },
255];
256
257/// Wire type strings + field hints for Swift (flat Codable stays in Models.swift).
258pub fn emit_wire_catalog_swift() -> String {
259    let mut out = String::from(
260        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
261         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
262         // Runtime decode still uses flat `WidgetElement` in Models.swift.\n\n\
263         import Foundation\n\n\
264         /// Known `type` wire values for widget IR elements.\n\
265         public enum WidgetWireType: String, CaseIterable, Sendable {\n",
266    );
267    for spec in IR_ELEMENTS {
268        let case = swift_case_name(spec.wire);
269        out.push_str(&format!("    case {case} = \"{}\"\n", spec.wire));
270    }
271    out.push_str("    case spacer = \"spacer\"\n}\n\n");
272    out.push_str("/// Per-element field hints (documentation / drift checks; not Codable).\n");
273    out.push_str("public enum WidgetWireFields {\n");
274    for spec in IR_ELEMENTS {
275        let case = swift_case_name(spec.wire);
276        let fields: Vec<String> = spec
277            .fields
278            .iter()
279            .map(|f| format!("\"{}\"", f.replace('\\', "\\\\").replace('"', "\\\"")))
280            .collect();
281        out.push_str(&format!(
282            "    public static let {case}: [String] = [{}]\n",
283            fields.join(", ")
284        ));
285    }
286    out.push_str("    public static let spacer: [String] = [\"minLength?: number\"]\n");
287    out.push_str("}\n");
288    out
289}
290
291/// Wire type constants for Android Glance renderer (JSONObject path stays dynamic).
292pub fn emit_wire_catalog_kotlin() -> String {
293    let mut out = String::from(
294        "// AUTO-GENERATED by `cargo run --bin gen-native --features codegen`\n\
295         // Do not edit. Source: `src/codegen.rs` IR_ELEMENTS.\n\
296         // Runtime still uses org.json.JSONObject via render.El.\n\n\
297         package git.s00d.widgets\n\n\
298         /** Known `type` wire values for widget IR elements. */\n\
299         object WireTypes {\n",
300    );
301    for spec in IR_ELEMENTS {
302        let const_name = kotlin_const_name(spec.wire);
303        out.push_str(&format!(
304            "    const val {const_name} = \"{}\"\n",
305            spec.wire
306        ));
307    }
308    out.push_str("    const val SPACER = \"spacer\"\n\n");
309    out.push_str("    val ALL: Set<String> = setOf(\n");
310    for spec in IR_ELEMENTS {
311        out.push_str(&format!("        {},\n", kotlin_const_name(spec.wire)));
312    }
313    out.push_str("        SPACER,\n    )\n}\n");
314    out
315}
316
317fn swift_case_name(wire: &str) -> String {
318    // camelCase identifiers from wire names (already lower).
319    wire.to_string()
320}
321
322fn kotlin_const_name(wire: &str) -> String {
323    wire.to_uppercase()
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use crate::capabilities::ELEMENT_TYPES;
330
331    #[test]
332    fn ir_spec_covers_all_element_types() {
333        let wires: Vec<&str> = IR_ELEMENTS
334            .iter()
335            .map(|e| e.wire)
336            .chain(["spacer"])
337            .collect();
338        for ty in ELEMENT_TYPES {
339            assert!(wires.contains(ty), "IR_ELEMENTS missing wire type `{ty}`");
340        }
341        assert_eq!(wires.len(), ELEMENT_TYPES.len());
342    }
343
344    #[test]
345    fn codegen_covers_all_element_types() {
346        let ts = emit_widget_types_ts();
347        for ty in ELEMENT_TYPES {
348            let needle = format!("type: \"{ty}\"");
349            assert!(
350                ts.contains(&needle),
351                "generated TS missing element type `{ty}`"
352            );
353        }
354        assert!(ts.contains("export interface WidgetConfig"));
355        assert!(ts.contains("export type WidgetElement"));
356        // Must not be a stale include_str twin
357        assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
358    }
359
360    #[test]
361    fn swift_catalog_covers_all_element_types() {
362        let swift = emit_wire_catalog_swift();
363        for ty in ELEMENT_TYPES {
364            assert!(
365                swift.contains(&format!("= \"{ty}\"")),
366                "Swift catalog missing `{ty}`"
367            );
368        }
369    }
370
371    #[test]
372    fn kotlin_catalog_covers_all_element_types() {
373        let kt = emit_wire_catalog_kotlin();
374        for ty in ELEMENT_TYPES {
375            assert!(
376                kt.contains(&format!("= \"{ty}\"")),
377                "Kotlin catalog missing `{ty}`"
378            );
379        }
380    }
381
382    #[test]
383    fn type_name_exhaustive() {
384        // Compiling this match fails if a WidgetElement variant is added without update.
385        use crate::models::{SpacerElement, WidgetElement};
386        fn check(el: &WidgetElement) -> &'static str {
387            el.type_name()
388        }
389        let _ = check(&WidgetElement::Spacer(SpacerElement { min_length: None }));
390    }
391}