Skip to main content

tauri_plugin_widgets/
apply.rs

1//! Outcomes for `set_widget_config` — no silent `Ok(true)`.
2
3use serde::{Deserialize, Serialize};
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6
7/// Why a config write was skipped.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(tag = "reason", rename_all = "camelCase")]
10pub enum SkipReason {
11    /// Store already holds identical JSON for this widget id.
12    Unchanged {
13        /// Hash of the unchanged config bytes.
14        hash: u64,
15    },
16}
17
18/// Result of a WidgetKit / AppWidget reload attempt.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(tag = "outcome", rename_all = "camelCase")]
21pub enum ReloadOutcome {
22    /// Native reload was invoked.
23    Ok,
24    /// Mobile release throttle skipped the reload.
25    Throttled {
26        /// Seconds until the next reload is allowed.
27        #[serde(rename = "remainingSecs")]
28        remaining_secs: u64,
29    },
30    /// Reload intentionally not called.
31    Skipped {
32        /// Human-readable why (`unchanged`, `skip_reload`, …).
33        why: String,
34    },
35    /// Reload was attempted but failed.
36    Failed {
37        /// Error message.
38        error: String,
39    },
40}
41
42/// Full result of [`crate::desktop::Widget::set_widget_config`] /
43/// [`crate::mobile::Widget::set_widget_config`].
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct ApplyOutcome {
47    /// `true` when the config map was written (bytes differed from store).
48    pub written: bool,
49    /// What happened on the native reload path.
50    pub reload: ReloadOutcome,
51    /// Transport names that received the map (empty when `written` is false).
52    pub transports: Vec<String>,
53    /// Present when the write was skipped.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub skip: Option<SkipReason>,
56}
57
58impl ApplyOutcome {
59    /// Unchanged store — push may still fire on desktop; reload skipped.
60    pub fn unchanged(hash: u64) -> Self {
61        Self {
62            written: false,
63            reload: ReloadOutcome::Skipped {
64                why: "unchanged".into(),
65            },
66            transports: Vec::new(),
67            skip: Some(SkipReason::Unchanged { hash }),
68        }
69    }
70}
71
72/// Stable content hash for config JSON bytes (dedup vs store).
73pub fn config_content_hash(json: &str) -> u64 {
74    let mut hasher = DefaultHasher::new();
75    json.hash(&mut hasher);
76    hasher.finish()
77}
78
79/// Seconds remaining until the next reload is allowed, if currently throttled.
80pub fn throttle_remaining_secs(elapsed_secs: u64, min_interval: u64) -> Option<u64> {
81    if min_interval == 0 || elapsed_secs >= min_interval {
82        None
83    } else {
84        Some(min_interval.saturating_sub(elapsed_secs))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn unchanged_serializes_camel_case() {
94        let o = ApplyOutcome::unchanged(42);
95        let v = serde_json::to_value(&o).unwrap();
96        assert_eq!(v["written"], false);
97        assert_eq!(v["reload"]["outcome"], "skipped");
98        assert_eq!(v["skip"]["reason"], "unchanged");
99        assert_eq!(v["skip"]["hash"], 42);
100    }
101
102    #[test]
103    fn throttled_remaining_secs_camel() {
104        let r = ReloadOutcome::Throttled {
105            remaining_secs: 840,
106        };
107        let v = serde_json::to_value(&r).unwrap();
108        assert_eq!(v["outcome"], "throttled");
109        assert_eq!(v["remainingSecs"], 840);
110    }
111
112    #[test]
113    fn throttle_remaining_math() {
114        assert_eq!(throttle_remaining_secs(0, 0), None);
115        assert_eq!(throttle_remaining_secs(100, 0), None);
116        assert_eq!(throttle_remaining_secs(60, 900), Some(840));
117        assert_eq!(throttle_remaining_secs(900, 900), None);
118        assert_eq!(throttle_remaining_secs(901, 900), None);
119    }
120
121    #[test]
122    fn same_json_same_hash() {
123        assert_eq!(
124            config_content_hash(r#"{"version":1}"#),
125            config_content_hash(r#"{"version":1}"#)
126        );
127        assert_ne!(
128            config_content_hash(r#"{"version":1}"#),
129            config_content_hash(r#"{"version":2}"#)
130        );
131    }
132}