Skip to main content

tauri_plugin_widgets/desktop/
sources.rs

1//! Host-registered data sources (eww `defpoll` / `deflisten`), Rust API only: a closure
2//! polled on an interval, or a channel / iterator, feeding `set_widget_data`.
3
4use std::time::Duration;
5
6use tauri::{Emitter, Manager, Runtime};
7
8use super::Widget;
9
10impl<R: Runtime> Widget<R> {
11    /// `setWidgetData` from Rust: store `data` for `{{data.…}}`, push it to open widget
12    /// windows and reload native widgets. Unchanged data is a no-op (`Ok(false)`).
13    /// `group: None` uses the configured default group.
14    pub fn set_widget_data(
15        &self,
16        widget_id: &str,
17        data: &serde_json::Value,
18        instance: Option<&str>,
19        group: Option<&str>,
20    ) -> crate::Result<bool> {
21        let group = crate::group::resolve_group(group)?;
22        crate::group::validate_widget_id(widget_id)?;
23        let instance = crate::group::validate_instance_id(instance)?;
24        let key = crate::store::data_key(widget_id, instance);
25        let raw = if data.is_null() {
26            String::new()
27        } else {
28            serde_json::to_string(data)?
29        };
30        if raw.len() > crate::resolve::MAX_SOURCE_BYTES {
31            return Err(crate::Error::new(format!(
32                "widget data is {} bytes; the limit is {}",
33                raw.len(),
34                crate::resolve::MAX_SOURCE_BYTES
35            )));
36        }
37        if self.get_items(&key, &group)?.as_deref() == Some(raw.as_str()) {
38            return Ok(false);
39        }
40        self.set_items(&key, &raw, &group)?;
41        self.data_changed(&group, widget_id, instance);
42        let _ = self.app.emit(
43            "widget-data-push",
44            serde_json::json!({
45                "group": group,
46                "widgetId": widget_id,
47                "instance": instance,
48                "data": data,
49            }),
50        );
51        self.reload_all_timelines()?;
52        Ok(true)
53    }
54
55    /// Call `poll` every `interval` (first call right away) and store the result as the
56    /// widget's data (default group). Registering a widget id again replaces its source.
57    ///
58    /// ```no_run
59    /// # fn f<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
60    /// use tauri_plugin_widgets::WidgetExt;
61    /// app.widget().register_poll_source("cpu", std::time::Duration::from_secs(2), || {
62    ///     serde_json::json!({ "load": 0.42 })
63    /// });
64    /// # }
65    /// ```
66    pub fn register_poll_source<F>(&self, widget_id: &str, interval: Duration, mut poll: F)
67    where
68        F: FnMut() -> serde_json::Value + Send + 'static,
69    {
70        let interval = interval.max(Duration::from_millis(100));
71        self.spawn_source(widget_id, move |alive, push| {
72            while alive() {
73                push(poll());
74                std::thread::sleep(interval);
75            }
76        });
77    }
78
79    /// Store every value `values` yields (a `std::sync::mpsc::Receiver`, any blocking
80    /// iterator) as the widget's data until it ends or the source is replaced.
81    pub fn register_listen_source<I>(&self, widget_id: &str, values: I)
82    where
83        I: IntoIterator<Item = serde_json::Value>,
84        I::IntoIter: Send + 'static,
85    {
86        let iter = values.into_iter();
87        self.spawn_source(widget_id, move |alive, push| {
88            for v in iter {
89                if !alive() {
90                    break;
91                }
92                push(v);
93            }
94        });
95    }
96
97    /// Stop a poll / listen source. A listen source stops at its next value.
98    pub fn unregister_source(&self, widget_id: &str) -> bool {
99        self.sources.lock().unwrap().remove(widget_id).is_some()
100    }
101
102    fn spawn_source<B>(&self, widget_id: &str, body: B)
103    where
104        B: FnOnce(&dyn Fn() -> bool, &dyn Fn(serde_json::Value)) + Send + 'static,
105    {
106        // Never reused, so a stopped thread cannot mistake a newer source for its own.
107        static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
108        let gen = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
109        self.sources
110            .lock()
111            .unwrap()
112            .insert(widget_id.to_string(), gen);
113        let app = self.app.clone();
114        let id = widget_id.to_string();
115        std::thread::spawn(move || {
116            let alive = || {
117                app.try_state::<Widget<R>>()
118                    .is_some_and(|w| w.sources.lock().unwrap().get(&id) == Some(&gen))
119            };
120            let push = |v: serde_json::Value| {
121                if let Some(w) = app.try_state::<Widget<R>>() {
122                    if let Err(e) = w.set_widget_data(&id, &v, None, None) {
123                        log::warn!("widgets: data source {id}: {e}");
124                    }
125                }
126            };
127            body(&alive, &push);
128            if let Some(w) = app.try_state::<Widget<R>>() {
129                let mut s = w.sources.lock().unwrap();
130                if s.get(&id) == Some(&gen) {
131                    s.remove(&id);
132                }
133            }
134        });
135    }
136}