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 "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
257pub 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
291pub 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 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 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 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}