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