Skip to main content

tauri_plugin_widgets/
lib.rs

1//! # tauri-plugin-widgets
2//!
3//! A Tauri v2 plugin for building native widgets on Android, iOS, macOS,
4//! Windows, and Linux from a single JSON UI configuration.
5//!
6//! Empty placeholders for Adaptive Cards are avoided when the `rasterize`
7//! feature is enabled (chart/canvas/gauge/shape/zstack/gradients → PNG).
8//!
9//! ## Overview
10//!
11//! - **Widget Config API** — send a declarative [`WidgetConfig`](models::WidgetConfig)
12//!   describing layouts and elements. The native widget renders it using
13//!   SwiftUI (Apple), RemoteViews (Android), or HTML/CSS (desktop).
14//!
15//! - **Data API** — key-value storage shared with native widget extensions
16//!   via the App Group shared container (Apple), SharedPreferences (Android),
17//!   or JSON files (desktop).
18//!
19//! - **Desktop widget windows** — frameless, transparent Tauri webview windows
20//!   that render the same JSON config as HTML/CSS.
21//!
22//! ## Architecture
23//!
24//! The plugin acts as a **library**, not a builder. It does NOT compile or
25//! inject widget extensions at runtime. Instead, it provides:
26//!
27//! 1. **Rust side** — commands for data storage and WidgetKit reload
28//! 2. **Swift Package** (`swift/TauriWidgets`) — public SwiftUI views and
29//!    models that developers import into their own Widget Extension target
30//! 3. **Templates** (`templates/`) — ready-to-use scripts and Swift files
31//!
32//! This follows Apple's guidelines: the extension is built by Xcode, signed
33//! with the developer's certificate, and included in the app bundle at
34//! compile time.
35//!
36//! ## Quick Start (Rust)
37//!
38//! ```no_run
39//! tauri::Builder::default()
40//!     .plugin(tauri_plugin_widgets::init());
41//! ```
42//!
43//! ## iOS Setup
44//!
45//! 1. Open `gen/apple/*.xcodeproj` in Xcode
46//! 2. File → New → Target → Widget Extension
47//! 3. Add `swift/` as a Local Swift Package dependency
48//! 4. Add `TauriWidgets` library to the Widget Extension target
49//! 5. Enable **App Groups** in both targets (App + Widget Extension)
50//! 6. Use the template from `templates/ios-widget/MyWidget.swift`
51//!
52//! ## macOS Setup ("Satellite Project")
53//!
54//! Tauri for macOS does not generate an `.xcodeproj`, so the widget
55//! extension must be built as a separate Xcode project:
56//!
57//! 1. Create `src-tauri/widget-extension/` with an Xcode project
58//!    containing a Widget Extension target
59//! 2. Add `swift/` as a Local Swift Package dependency
60//! 3. Enable **App Groups** in both the main app entitlements and
61//!    the widget extension entitlements
62//! 4. `build-widget.sh` runs via `beforeBundleCommand` (builds + signs `.appex`)
63//! 5. `bundle.macOS.files` copies the `.appex` into `Contents/PlugIns/`
64//!    during a normal `tauri build` (Tauri nested-codesigns PlugIns)
65//! 6. `plugins.widgets.transport` defaults to `widgetContainer` on macOS (works ad-hoc
66//!    and Developer ID); set `appGroup` only for a sandboxed Mac App Store host
67//!
68//! ## Rust API
69//!
70//! Build a config with typed helpers (compile-checked, not executed here):
71//!
72//! ```
73//! use tauri_plugin_widgets::models::{text, vstack, WidgetConfig};
74//!
75//! let _cfg = WidgetConfig::small(vstack(vec![
76//!     text("72°").font_size(36.0).into(),
77//! ]));
78//! ```
79//!
80//! Then call [`WidgetExt::widget`] on an `AppHandle` to `set_widget_config` /
81//! `reload_all_timelines` (requires a running Tauri app).
82
83#![cfg_attr(docsrs, feature(doc_cfg))]
84#![warn(missing_docs)]
85
86#[cfg(mobile)]
87use tauri::RunEvent;
88use tauri::{
89    plugin::{Builder, TauriPlugin},
90    Manager, Runtime,
91};
92
93#[cfg(desktop)]
94use std::borrow::Cow;
95
96#[cfg(desktop)]
97#[allow(missing_docs)]
98#[cfg_attr(docsrs, doc(cfg(desktop)))]
99pub mod desktop;
100#[cfg(mobile)]
101#[allow(missing_docs)]
102#[cfg_attr(docsrs, doc(cfg(mobile)))]
103pub mod mobile;
104
105/// Adaptive Cards transpiler (Windows Widgets Board).
106#[allow(missing_docs)]
107pub mod adaptive_card;
108/// Outcomes for `set_widget_config` (written / reload / skip).
109pub mod apply;
110/// Element × platform capability matrix.
111#[allow(missing_docs)]
112pub mod capabilities;
113/// TypeScript IR emitter (`gen-ts`).
114#[allow(missing_docs)]
115pub mod codegen;
116mod commands;
117/// Plugin configuration (`plugins.widgets` in `tauri.conf.json`).
118pub mod config;
119/// Plugin error type.
120pub mod error;
121/// Fail-closed validation for widget `group` identifiers.
122pub mod group;
123/// SF Symbol → Material / emoji resolve for non-Apple hosts.
124pub mod icons;
125/// Host-side remote image prefetch for WidgetKit / desktop store writes.
126pub mod image_prefetch;
127/// Widget IR models (`WidgetConfig`, `WidgetElement`, …).
128///
129/// Element structs and their fields carry rustdoc used by `schemars` / docs site.
130pub mod models;
131/// Host-side IR normalization (`textStyle` → points, semantic colors → adaptive hex).
132pub mod normalize;
133/// SVG / PNG helpers for Adaptive Cards fallbacks.
134#[allow(missing_docs)]
135pub mod rasterize;
136/// Render receipts written by native / desktop surfaces.
137pub mod receipt;
138/// Render-time resolution: scheduled entries and `{{…}}` placeholders.
139pub mod resolve;
140/// Canonical layout dumps for snapshot tests.
141pub mod snapshot;
142/// Shared key-value store helpers and action envelopes.
143pub mod store;
144/// Host black-box journal (`WIDGET_DEBUG` / debug builds).
145pub mod trace;
146/// macOS / desktop config transport selection.
147pub mod transport;
148
149#[cfg(target_os = "windows")]
150#[allow(missing_docs)]
151#[cfg_attr(docsrs, doc(cfg(windows)))]
152pub mod windows;
153
154#[cfg(all(target_os = "linux", feature = "linux"))]
155#[allow(missing_docs)]
156#[cfg_attr(docsrs, doc(cfg(all(target_os = "linux", feature = "linux"))))]
157pub mod linux;
158
159#[cfg(target_os = "macos")]
160#[allow(missing_docs)]
161#[cfg_attr(docsrs, doc(cfg(macos)))]
162pub mod macos_transport;
163
164pub use adaptive_card::{to_adaptive_card, to_adaptive_card_for_size, TranspileResult};
165pub use apply::{ApplyOutcome, ReloadOutcome, SkipReason};
166pub use config::{TransportKind, WidgetsPluginConfig};
167pub use error::{Error, Result};
168pub use receipt::{SkippedElement, WidgetRenderReceipt};
169pub use store::WidgetActionEnvelope;
170pub use trace::{TraceEntry, TraceEvent, WidgetTrace};
171pub use transport::{Receipt, Transport};
172
173#[cfg(desktop)]
174pub use desktop::Widget;
175#[cfg(mobile)]
176pub use mobile::Widget;
177
178/// Extension trait for convenient access from any Tauri manager.
179pub trait WidgetExt<R: Runtime> {
180    /// Returns the managed [`Widget`] state.
181    fn widget(&self) -> &Widget<R>;
182}
183
184impl<R: Runtime, T: Manager<R>> WidgetExt<R> for T {
185    fn widget(&self) -> &Widget<R> {
186        self.state::<Widget<R>>().inner()
187    }
188}
189
190/// Initialize the widgets plugin. Register it with `tauri::Builder::plugin()`.
191pub fn init<R: Runtime>() -> TauriPlugin<R, Option<WidgetsPluginConfig>> {
192    let builder = Builder::<R, Option<WidgetsPluginConfig>>::new("widgets")
193        .invoke_handler(tauri::generate_handler![
194            commands::set_items,
195            commands::get_items,
196            commands::set_register_widget,
197            commands::reload_all_timelines,
198            commands::reload_timelines,
199            commands::request_widget,
200            commands::create_widget_window,
201            commands::close_widget_window,
202            commands::set_widget_config,
203            commands::set_widget_config_from_path,
204            commands::get_widget_config,
205            commands::widget_action,
206            commands::poll_pending_actions,
207            commands::report_receipt,
208            commands::get_widget_diagnostics,
209            commands::get_widget_trace,
210            commands::flush_widget_trace,
211            commands::set_widget_data,
212            commands::set_widget_instances,
213            commands::get_active_widgets,
214            commands::start_activity,
215            commands::update_activity,
216            commands::end_activity,
217            commands::list_activities,
218            commands::activity_island,
219            commands::get_activity_push_to_start_token,
220            commands::set_widget_state,
221            commands::get_widget_state,
222            commands::register_activity_template,
223            commands::get_widget_push_token,
224            commands::can_promote_activities,
225            commands::open_activity_promotion_settings,
226        ])
227        .setup(|app, api| {
228            #[cfg(mobile)]
229            let widget = mobile::init(app, api)?;
230            #[cfg(desktop)]
231            let widget = desktop::init(app, api)?;
232            app.manage(widget);
233            Ok(())
234        });
235
236    #[cfg(mobile)]
237    let builder = builder.on_event(|app, event| match event {
238        RunEvent::Ready | RunEvent::Resumed => {
239            if let Some(widget) = app.try_state::<Widget<R>>() {
240                widget.inner().drain_pending_actions_to_events();
241            }
242        }
243        _ => {}
244    });
245
246    // macOS: host writes go to the App Group files from a writer thread; finish them
247    // before the process ends.
248    #[cfg(target_os = "macos")]
249    let builder = builder.on_event(|app, event| {
250        if let tauri::RunEvent::Exit = event {
251            if let Some(widget) = app.try_state::<Widget<R>>() {
252                widget.inner().flush_store();
253            }
254        }
255    });
256
257    #[cfg(desktop)]
258    let builder =
259        builder.register_uri_scheme_protocol(desktop::BUILTIN_PROTOCOL, |_app, _request| {
260            const HTML: &[u8] = include_bytes!("../widget.html");
261            tauri::http::Response::builder()
262                .header("content-type", "text/html; charset=utf-8")
263                .body(Cow::Borrowed(HTML))
264                .unwrap()
265        });
266
267    builder.build()
268}