1pub 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 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
51const 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
256pub 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
290pub 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 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 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 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}