1use crate::models::{WidgetConfig, WidgetElement, VStackElement, HStackElement, ZStackElement, GridElement, ContainerElement, TextElement, ImageElement, ProgressElement, GaugeElement, ButtonElement, ToggleElement, DividerElement, DateElement, ChartElement, ListElement, LinkElement, ShapeElement, TimerElement, CanvasElement, LabelElement};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum WidgetPlatform {
11 Ios,
12 Macos,
13 Android,
14 Desktop,
15 Windows,
17}
18
19impl WidgetPlatform {
20 pub fn all() -> [WidgetPlatform; 5] {
21 [
22 WidgetPlatform::Ios,
23 WidgetPlatform::Macos,
24 WidgetPlatform::Android,
25 WidgetPlatform::Desktop,
26 WidgetPlatform::Windows,
27 ]
28 }
29
30 pub fn as_str(self) -> &'static str {
31 match self {
32 WidgetPlatform::Ios => "ios",
33 WidgetPlatform::Macos => "macos",
34 WidgetPlatform::Android => "android",
35 WidgetPlatform::Desktop => "desktop",
36 WidgetPlatform::Windows => "windows",
37 }
38 }
39
40 pub fn current() -> WidgetPlatform {
42 if cfg!(target_os = "ios") {
43 WidgetPlatform::Ios
44 } else if cfg!(target_os = "macos") {
45 WidgetPlatform::Macos
46 } else if cfg!(target_os = "android") {
47 WidgetPlatform::Android
48 } else if cfg!(target_os = "windows") {
49 WidgetPlatform::Windows
51 } else {
52 WidgetPlatform::Desktop
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Support {
60 Full,
61 Degraded,
62 Unsupported,
63}
64
65impl Support {
66 pub fn as_str(self) -> &'static str {
67 match self {
68 Support::Full => "full",
69 Support::Degraded => "degraded",
70 Support::Unsupported => "unsupported",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
77pub struct CapabilityEntry {
78 pub element: &'static str,
79 pub platform: WidgetPlatform,
80 pub support: Support,
81 pub note: &'static str,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CapabilityWarning {
87 pub path: String,
88 pub element: String,
89 pub platform: WidgetPlatform,
90 pub support: Support,
91 pub note: String,
92}
93
94pub const ELEMENT_TYPES: &[&str] = &[
96 "vstack",
97 "hstack",
98 "zstack",
99 "grid",
100 "container",
101 "text",
102 "image",
103 "progress",
104 "gauge",
105 "button",
106 "toggle",
107 "divider",
108 "spacer",
109 "date",
110 "chart",
111 "list",
112 "link",
113 "shape",
114 "timer",
115 "canvas",
116 "label",
117];
118
119pub use crate::snapshot::{CORE_ELEMENTS, EXTENDED_ELEMENTS};
120pub const FEATURE_KEYS: &[&str] = &[
122 "image.url",
123 "image.systemName",
124 "background.gradient",
125 "canvas.path",
126 "timer.live",
127];
128
129fn cell(
130 element: &'static str,
131 platform: WidgetPlatform,
132 support: Support,
133 note: &'static str,
134) -> CapabilityEntry {
135 CapabilityEntry {
136 element,
137 platform,
138 support,
139 note,
140 }
141}
142
143fn apple_full(element: &'static str) -> [CapabilityEntry; 2] {
144 [
145 cell(element, WidgetPlatform::Ios, Support::Full, ""),
146 cell(element, WidgetPlatform::Macos, Support::Full, ""),
147 ]
148}
149
150pub fn capability_table() -> Vec<CapabilityEntry> {
152 use Support::*;
153 use WidgetPlatform::*;
154
155 let mut out = Vec::with_capacity(ELEMENT_TYPES.len() * 5 + FEATURE_KEYS.len() * 5);
156
157 let layout = [
158 "vstack",
159 "hstack",
160 "grid",
161 "container",
162 "text",
163 "spacer",
164 "divider",
165 "label",
166 "progress",
167 "button",
168 "toggle",
169 "date",
170 "link",
171 ];
172 for el in layout {
173 out.extend_from_slice(&apple_full(el));
174 out.push(cell(el, Android, Full, ""));
175 out.push(cell(el, Desktop, Full, ""));
176 out.push(cell(el, Windows, Full, "Adaptive Cards 1.5"));
177 }
178
179 out.extend_from_slice(&apple_full("zstack"));
181 out.push(cell("zstack", Android, Full, ""));
182 out.push(cell("zstack", Desktop, Full, ""));
183 out.push(cell(
184 "zstack",
185 Windows,
186 Degraded,
187 "flattened Container, no overlay",
188 ));
189
190 out.extend_from_slice(&apple_full("shape"));
191 out.push(cell("shape", Android, Full, ""));
192 out.push(cell("shape", Desktop, Full, ""));
193 out.push(cell("shape", Windows, Degraded, "rasterized PNG"));
194
195 out.extend_from_slice(&apple_full("gauge"));
196 out.push(cell("gauge", Android, Full, ""));
197 out.push(cell("gauge", Desktop, Full, ""));
198 out.push(cell("gauge", Windows, Degraded, "rasterized PNG"));
199
200 out.extend_from_slice(&apple_full("image"));
202 out.push(cell(
203 "image",
204 Android,
205 Degraded,
206 "systemName via glyph map; url via localPath preprocess",
207 ));
208 out.push(cell("image", Desktop, Full, ""));
209 out.push(cell(
210 "image",
211 Windows,
212 Degraded,
213 "url/data URI; systemName unsupported",
214 ));
215
216 out.extend_from_slice(&apple_full("chart"));
217 out.push(cell(
218 "chart",
219 Android,
220 Degraded,
221 "simplified bar/line rendering",
222 ));
223 out.push(cell("chart", Desktop, Full, "SVG"));
224 out.push(cell("chart", Windows, Degraded, "rasterized PNG"));
225
226 out.extend_from_slice(&apple_full("list"));
227 out.push(cell(
228 "list",
229 Android,
230 Full,
231 "Glance LazyColumn; depth/children limited",
232 ));
233 out.push(cell("list", Desktop, Full, ""));
234 out.push(cell("list", Windows, Degraded, "flattened TextBlocks"));
235
236 out.extend_from_slice(&apple_full("timer"));
237 out.push(cell(
238 "timer",
239 Android,
240 Degraded,
241 "static snapshot, not live Chronometer in all hosts",
242 ));
243 out.push(cell("timer", Desktop, Full, "setInterval"));
244 out.push(cell(
245 "timer",
246 Windows,
247 Degraded,
248 "static TextBlock of targetDate",
249 ));
250
251 out.extend_from_slice(&apple_full("canvas"));
252 out.push(cell(
253 "canvas",
254 Android,
255 Degraded,
256 "bitmap canvas; path support limited",
257 ));
258 out.push(cell("canvas", Desktop, Full, "SVG"));
259 out.push(cell("canvas", Windows, Degraded, "rasterized PNG"));
260
261 out.push(cell(
263 "image.url",
264 Ios,
265 Unsupported,
266 "prefetch into shared container not wired",
267 ));
268 out.push(cell(
269 "image.url",
270 Macos,
271 Unsupported,
272 "prefetch into shared container not wired",
273 ));
274 out.push(cell(
275 "image.url",
276 Android,
277 Full,
278 "preprocess to localPath on setWidgetConfig",
279 ));
280 out.push(cell("image.url", Desktop, Full, ""));
281 out.push(cell("image.url", Windows, Full, "Adaptive Cards Image.url"));
282
283 out.push(cell("image.systemName", Ios, Full, "SF Symbols"));
284 out.push(cell("image.systemName", Macos, Full, "SF Symbols"));
285 out.push(cell(
286 "image.systemName",
287 Android,
288 Degraded,
289 "glyph / drawable name map",
290 ));
291 out.push(cell(
292 "image.systemName",
293 Desktop,
294 Degraded,
295 "placeholder glyph",
296 ));
297 out.push(cell(
298 "image.systemName",
299 Windows,
300 Unsupported,
301 "no SF Symbols on Adaptive Cards",
302 ));
303
304 out.push(cell(
305 "background.gradient",
306 Ios,
307 Degraded,
308 "linear primary; radial/angular limited",
309 ));
310 out.push(cell(
311 "background.gradient",
312 Macos,
313 Degraded,
314 "linear primary; radial/angular limited",
315 ));
316 out.push(cell(
317 "background.gradient",
318 Android,
319 Degraded,
320 "first color stop only (Glance)",
321 ));
322 out.push(cell(
323 "background.gradient",
324 Desktop,
325 Full,
326 "linear/radial/angular CSS/SVG",
327 ));
328 out.push(cell(
329 "background.gradient",
330 Windows,
331 Unsupported,
332 "Container style=emphasis only",
333 ));
334
335 out.push(cell("canvas.path", Ios, Degraded, "M/L/H/V/Z subset"));
336 out.push(cell("canvas.path", Macos, Degraded, "M/L/H/V/Z subset"));
337 out.push(cell(
338 "canvas.path",
339 Android,
340 Degraded,
341 "limited path commands",
342 ));
343 out.push(cell("canvas.path", Desktop, Full, "SVG path"));
344 out.push(cell("canvas.path", Windows, Degraded, "rasterized via SVG"));
345
346 out.push(cell("timer.live", Ios, Full, "Text(..., .timer)"));
347 out.push(cell("timer.live", Macos, Full, "Text(..., .timer)"));
348 out.push(cell(
349 "timer.live",
350 Android,
351 Unsupported,
352 "no live timer in Glance snapshot",
353 ));
354 out.push(cell("timer.live", Desktop, Full, "JS interval"));
355 out.push(cell("timer.live", Windows, Unsupported, "static only"));
356
357 out
358}
359
360use std::sync::OnceLock;
361
362pub fn support_for(element: &str, platform: WidgetPlatform) -> CapabilityEntry {
364 static TABLE: OnceLock<Vec<CapabilityEntry>> = OnceLock::new();
365 let table = TABLE.get_or_init(capability_table);
366 table
367 .iter()
368 .find(|e| e.element == element && e.platform == platform)
369 .copied()
370 .unwrap_or(CapabilityEntry {
371 element: "",
372 platform,
373 support: Support::Unsupported,
374 note: "unknown element",
375 })
376}
377
378impl WidgetElement {
379 pub fn type_name(&self) -> &'static str {
381 match self {
382 WidgetElement::VStack(_) => "vstack",
383 WidgetElement::HStack(_) => "hstack",
384 WidgetElement::ZStack(_) => "zstack",
385 WidgetElement::Grid(_) => "grid",
386 WidgetElement::Container(_) => "container",
387 WidgetElement::Text(_) => "text",
388 WidgetElement::Image(_) => "image",
389 WidgetElement::Progress(_) => "progress",
390 WidgetElement::Gauge(_) => "gauge",
391 WidgetElement::Button(_) => "button",
392 WidgetElement::Toggle(_) => "toggle",
393 WidgetElement::Divider(_) => "divider",
394 WidgetElement::Spacer(_) => "spacer",
395 WidgetElement::Date(_) => "date",
396 WidgetElement::Chart(_) => "chart",
397 WidgetElement::List(_) => "list",
398 WidgetElement::Link(_) => "link",
399 WidgetElement::Shape(_) => "shape",
400 WidgetElement::Timer(_) => "timer",
401 WidgetElement::Canvas(_) => "canvas",
402 WidgetElement::Label(_) => "label",
403 }
404 }
405
406 fn children_ref(&self) -> &[WidgetElement] {
407 match self {
408 WidgetElement::VStack(VStackElement { children, .. })
409 | WidgetElement::HStack(HStackElement { children, .. })
410 | WidgetElement::ZStack(ZStackElement { children, .. })
411 | WidgetElement::Grid(GridElement { children, .. })
412 | WidgetElement::Container(ContainerElement { children, .. })
413 | WidgetElement::Link(LinkElement { children, .. }) => children,
414 _ => &[],
415 }
416 }
417
418 fn style_background_is_gradient(&self) -> bool {
419 use crate::models::{BackgroundValue, ElementStyle};
420 let style: Option<&ElementStyle> = match self {
421 WidgetElement::VStack(VStackElement { style, .. })
422 | WidgetElement::HStack(HStackElement { style, .. })
423 | WidgetElement::ZStack(ZStackElement { style, .. })
424 | WidgetElement::Grid(GridElement { style, .. })
425 | WidgetElement::Container(ContainerElement { style, .. })
426 | WidgetElement::Text(TextElement { style, .. })
427 | WidgetElement::Image(ImageElement { style, .. })
428 | WidgetElement::Progress(ProgressElement { style, .. })
429 | WidgetElement::Gauge(GaugeElement { style, .. })
430 | WidgetElement::Button(ButtonElement { style, .. })
431 | WidgetElement::Toggle(ToggleElement { style, .. })
432 | WidgetElement::Divider(DividerElement { style, .. })
433 | WidgetElement::Date(DateElement { style, .. })
434 | WidgetElement::Chart(ChartElement { style, .. })
435 | WidgetElement::List(ListElement { style, .. })
436 | WidgetElement::Link(LinkElement { style, .. })
437 | WidgetElement::Shape(ShapeElement { style, .. })
438 | WidgetElement::Timer(TimerElement { style, .. })
439 | WidgetElement::Canvas(CanvasElement { style, .. })
440 | WidgetElement::Label(LabelElement { style, .. }) => Some(style),
441 WidgetElement::Spacer(_) => None,
442 };
443 matches!(
444 style.and_then(|s| s.background.as_ref()),
445 Some(BackgroundValue::Gradient(_))
446 )
447 }
448}
449
450fn push_warn(
451 out: &mut Vec<CapabilityWarning>,
452 path: &str,
453 element: &str,
454 platform: WidgetPlatform,
455) {
456 let entry = support_for(element, platform);
457 if entry.support == Support::Full {
458 return;
459 }
460 out.push(CapabilityWarning {
461 path: path.to_string(),
462 element: element.to_string(),
463 platform,
464 support: entry.support,
465 note: entry.note.to_string(),
466 });
467}
468
469fn walk_element(
470 el: &WidgetElement,
471 path: &str,
472 platform: WidgetPlatform,
473 out: &mut Vec<CapabilityWarning>,
474) {
475 let ty = el.type_name();
476 push_warn(out, path, ty, platform);
477
478 if el.style_background_is_gradient() {
479 push_warn(out, path, "background.gradient", platform);
480 }
481
482 if let WidgetElement::Image(ImageElement {
483 url, system_name, ..
484 }) = el
485 {
486 if url.as_ref().map(|s| !s.is_empty()).unwrap_or(false) {
487 push_warn(out, path, "image.url", platform);
488 }
489 if system_name.as_ref().map(|s| !s.is_empty()).unwrap_or(false) {
490 push_warn(out, path, "image.systemName", platform);
491 }
492 }
493
494 if let WidgetElement::Timer(_) = el {
495 push_warn(out, path, "timer.live", platform);
496 }
497
498 if let WidgetElement::Canvas(CanvasElement { elements, .. }) = el {
499 if elements
500 .iter()
501 .any(|c| matches!(c, crate::models::CanvasDrawCommand::Path { .. }))
502 {
503 push_warn(out, path, "canvas.path", platform);
504 }
505 }
506
507 for (i, child) in el.children_ref().iter().enumerate() {
508 walk_element(child, &format!("{path}/{ty}[{i}]"), platform, out);
509 }
510}
511
512pub fn validate_config(config: &WidgetConfig, platform: WidgetPlatform) -> Vec<CapabilityWarning> {
514 let mut out = Vec::new();
515 if let Some(el) = &config.small {
516 walk_element(el, "small", platform, &mut out);
517 }
518 if let Some(el) = &config.medium {
519 walk_element(el, "medium", platform, &mut out);
520 }
521 if let Some(el) = &config.large {
522 walk_element(el, "large", platform, &mut out);
523 }
524 out
525}
526
527pub fn log_capabilities(config: &WidgetConfig) {
529 let platform = WidgetPlatform::current();
530 for w in validate_config(config, platform) {
531 log::warn!(
532 "widget capability {}: {} at {} on {} — {}",
533 w.support.as_str(),
534 w.element,
535 w.path,
536 w.platform.as_str(),
537 w.note
538 );
539 }
540}
541
542pub fn render_capability_matrix_md() -> String {
544 let mut md = String::from(
545 "# Capability matrix (element × platform)\n\n\
546 Generated from `tauri_plugin_widgets::capabilities`. Do not edit by hand.\n\n\
547 ## Core (strict snapshot contract)\n\n\
548 | Element | iOS | macOS | Android | Desktop | Windows |\n\
549 |---------|-----|-------|---------|---------|----------|\n",
550 );
551
552 for el in CORE_ELEMENTS {
553 let ios = support_for(el, WidgetPlatform::Ios);
554 let mac = support_for(el, WidgetPlatform::Macos);
555 let and = support_for(el, WidgetPlatform::Android);
556 let desk = support_for(el, WidgetPlatform::Desktop);
557 let win = support_for(el, WidgetPlatform::Windows);
558 md.push_str(&format!(
559 "| `{el}` | {} | {} | {} | {} | {} |\n",
560 cell_md(&ios),
561 cell_md(&mac),
562 cell_md(&and),
563 cell_md(&desk),
564 cell_md(&win),
565 ));
566 }
567
568 md.push_str(
569 "\n## Extended (best-effort, platform-dependent)\n\n\
570 | Element | iOS | macOS | Android | Desktop | Windows |\n\
571 |---------|-----|-------|---------|---------|----------|\n",
572 );
573 for el in EXTENDED_ELEMENTS {
574 let ios = support_for(el, WidgetPlatform::Ios);
575 let mac = support_for(el, WidgetPlatform::Macos);
576 let and = support_for(el, WidgetPlatform::Android);
577 let desk = support_for(el, WidgetPlatform::Desktop);
578 let win = support_for(el, WidgetPlatform::Windows);
579 md.push_str(&format!(
580 "| `{el}` | {} | {} | {} | {} | {} |\n",
581 cell_md(&ios),
582 cell_md(&mac),
583 cell_md(&and),
584 cell_md(&desk),
585 cell_md(&win),
586 ));
587 }
588
589 md.push_str("\n## Feature notes\n\n");
590 md.push_str("| Feature | iOS | macOS | Android | Desktop | Windows |\n");
591 md.push_str("|---------|-----|-------|---------|---------|----------|\n");
592 for feat in FEATURE_KEYS {
593 let ios = support_for(feat, WidgetPlatform::Ios);
594 let mac = support_for(feat, WidgetPlatform::Macos);
595 let and = support_for(feat, WidgetPlatform::Android);
596 let desk = support_for(feat, WidgetPlatform::Desktop);
597 let win = support_for(feat, WidgetPlatform::Windows);
598 md.push_str(&format!(
599 "| `{feat}` | {} | {} | {} | {} | {} |\n",
600 cell_md(&ios),
601 cell_md(&mac),
602 cell_md(&and),
603 cell_md(&desk),
604 cell_md(&win),
605 ));
606 }
607 md
608}
609
610fn cell_md(e: &CapabilityEntry) -> String {
611 if e.note.is_empty() {
612 e.support.as_str().to_string()
613 } else {
614 format!("{} ({})", e.support.as_str(), e.note)
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use crate::models::{
622 ChartDataPoint, ChartElement, ChartType, ImageElement, WidgetConfig, WidgetElement,
623 };
624
625 #[test]
626 fn all_element_types_have_five_platforms() {
627 for el in ELEMENT_TYPES {
628 for p in WidgetPlatform::all() {
629 let e = support_for(el, p);
630 assert_eq!(e.element, *el);
631 assert_eq!(e.platform, p);
632 }
633 }
634 }
635
636 #[test]
637 fn validate_flags_image_url_on_ios() {
638 let cfg = WidgetConfig {
639 version: 1,
640 small: Some(WidgetElement::Image(ImageElement {
641 system_name: None,
642 data: None,
643 url: Some("https://example.com/a.png".into()),
644 size: Some(32.0),
645 color: None,
646 content_mode: None,
647 style: Default::default(),
648 })),
649 medium: None,
650 large: None,
651 };
652 let warns = validate_config(&cfg, WidgetPlatform::Ios);
653 assert!(
654 warns
655 .iter()
656 .any(|w| w.element == "image.url" && w.support == Support::Unsupported),
657 "{warns:?}"
658 );
659 }
660
661 #[test]
662 fn matrix_markdown_mentions_vstack() {
663 let md = render_capability_matrix_md();
664 assert!(md.contains("`vstack`"));
665 assert!(md.contains("image.url"));
666 }
667
668 #[test]
669 fn chart_roundtrip_type_name() {
670 let el = WidgetElement::Chart(ChartElement {
671 chart_type: ChartType::Bar,
672 chart_data: vec![ChartDataPoint {
673 label: "a".into(),
674 value: 1.0,
675 color: None,
676 }],
677 tint: None,
678 style: Default::default(),
679 });
680 assert_eq!(el.type_name(), "chart");
681 }
682}
683
684#[cfg(test)]
685mod write_docs {
686 #[test]
687 fn capability_matrix_doc_matches() {
688 let expected = super::render_capability_matrix_md();
689 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
690 .join("docs/guide/_generated/capability-matrix.md");
691 if !path.exists() {
692 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
693 std::fs::write(&path, &expected).unwrap();
694 return;
695 }
696 let on_disk = std::fs::read_to_string(&path).unwrap();
697 assert_eq!(
698 on_disk, expected,
699 "docs/guide/_generated/capability-matrix.md drifted — regenerate with:\n\
700 cargo test --lib write_docs::capability_matrix_doc_matches -- --ignored\n\
701 or delete the file and re-run this test"
702 );
703 }
704}