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!("    const val {const_name} = \"{}\"\n", spec.wire));
304    }
305    out.push_str("    const val SPACER = \"spacer\"\n\n");
306    out.push_str("    val ALL: Set<String> = setOf(\n");
307    for spec in IR_ELEMENTS {
308        out.push_str(&format!("        {},\n", kotlin_const_name(spec.wire)));
309    }
310    out.push_str("        SPACER,\n    )\n}\n");
311    out
312}
313
314fn swift_case_name(wire: &str) -> String {
315    // camelCase identifiers from wire names (already lower).
316    wire.to_string()
317}
318
319fn kotlin_const_name(wire: &str) -> String {
320    wire.to_uppercase()
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::capabilities::ELEMENT_TYPES;
327
328    #[test]
329    fn ir_spec_covers_all_element_types() {
330        let wires: Vec<&str> = IR_ELEMENTS
331            .iter()
332            .map(|e| e.wire)
333            .chain(["spacer"])
334            .collect();
335        for ty in ELEMENT_TYPES {
336            assert!(wires.contains(ty), "IR_ELEMENTS missing wire type `{ty}`");
337        }
338        assert_eq!(wires.len(), ELEMENT_TYPES.len());
339    }
340
341    #[test]
342    fn codegen_covers_all_element_types() {
343        let ts = emit_widget_types_ts();
344        for ty in ELEMENT_TYPES {
345            let needle = format!("type: \"{ty}\"");
346            assert!(
347                ts.contains(&needle),
348                "generated TS missing element type `{ty}`"
349            );
350        }
351        assert!(ts.contains("export interface WidgetConfig"));
352        assert!(ts.contains("export type WidgetElement"));
353        // Must not be a stale include_str twin
354        assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
355    }
356
357    #[test]
358    fn swift_catalog_covers_all_element_types() {
359        let swift = emit_wire_catalog_swift();
360        for ty in ELEMENT_TYPES {
361            assert!(
362                swift.contains(&format!("= \"{ty}\"")),
363                "Swift catalog missing `{ty}`"
364            );
365        }
366    }
367
368    #[test]
369    fn kotlin_catalog_covers_all_element_types() {
370        let kt = emit_wire_catalog_kotlin();
371        for ty in ELEMENT_TYPES {
372            assert!(
373                kt.contains(&format!("= \"{ty}\"")),
374                "Kotlin catalog missing `{ty}`"
375            );
376        }
377    }
378
379    #[test]
380    fn type_name_exhaustive() {
381        // Compiling this match fails if a WidgetElement variant is added without update.
382        use crate::models::{SpacerElement, WidgetElement};
383        fn check(el: &WidgetElement) -> &'static str {
384            el.type_name()
385        }
386        let _ = check(&WidgetElement::Spacer(SpacerElement { min_length: None }));
387    }
388}