Skip to main content

tauri_plugin_widgets/
codegen.rs

1//! Emit TypeScript IR types from an explicit Rust IR_SPEC (SoT with models.rs).
2//!
3//! Not `include_str` of a hand-maintained twin — the emitter builds the file.
4//! Exhaustiveness is checked via [`WidgetElement`] match in tests / type_name.
5
6/// Emit `guest-js/generated/widget-types.ts`.
7pub 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    // Element interfaces — IR_SPEC mapping (must stay in sync with WidgetElement)
94    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    // Remaining elements that need custom bodies (list, canvas, etc. already partially in IR_ELEMENTS)
151    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
181/// Explicit IR → TS mapping. When adding a [`WidgetElement`] variant, extend this list
182/// and the exhaustive match in tests.
183const 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        // Must not be a stale include_str twin
417        assert!(ts.contains("Emitter: `src/codegen.rs` IR_SPEC"));
418    }
419
420    #[test]
421    fn type_name_exhaustive() {
422        // Compiling this match fails if a WidgetElement variant is added without update.
423        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}