Skip to main content

tauri_plugin_gpui/
lib.rs

1//! # tauri-plugin-gpui
2//!
3//! Render [GPUI](https://www.gpui.rs) views inside ordinary Tauri windows.
4//!
5//! Tauri/TAO stays authoritative for the application lifecycle, the event
6//! loop and native windows. This plugin permanently attaches GPUI as the
7//! content renderer of an existing Tauri window; all attached windows share
8//! one GPUI [`App`](gpui::App) running on Tauri's event-loop thread.
9//!
10//! ```
11//! use tauri_plugin_gpui::{GpuiWindowExt, gpui::{self, prelude::*}};
12//!
13//! struct Hello;
14//!
15//! impl gpui::Render for Hello {
16//!     fn render(&mut self, _: &mut gpui::Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
17//!         gpui::div().size_full().child("Hello from GPUI")
18//!     }
19//! }
20//!
21//! // In an app, `context` is `tauri::generate_context!()`.
22//! fn run(context: tauri::Context) {
23//!     tauri::Builder::default()
24//!         .setup(|app| {
25//!             tauri_plugin_gpui::init(app)?;
26//!             let window = tauri::WindowBuilder::new(app, "main").title("Example").build()?;
27//!             window.attach_gpui(|cx| cx.new(|_| Hello))?;
28//!             Ok(())
29//!         })
30//!         .run(context)
31//!         .expect("error while running tauri application");
32//! }
33//! ```
34//!
35//! # Platforms and features
36//!
37//! Linux, macOS and Windows work with default features. Android and iOS
38//! need the `mobile` feature; without it, [`init`] returns
39//! [`GpuiError::UnsupportedOperation`] on those targets. Embedding Tauri
40//! child webviews ([`webview_frame`]) is desktop-only.
41
42// GPUI's platform callback signatures are inherently nested closure types.
43#![allow(clippy::type_complexity)]
44#![warn(missing_docs)]
45#![cfg_attr(docsrs, feature(doc_cfg))]
46
47mod error;
48mod ffi;
49
50#[cfg(gpui_backend)]
51mod events;
52#[cfg(gpui_backend)]
53mod platform;
54#[cfg(gpui_backend)]
55mod runtime;
56#[cfg(all(gpui_backend, not(gpui_mobile)))]
57mod webview;
58
59#[cfg(gpui_android)]
60mod android;
61#[cfg(gpui_ios)]
62mod ios;
63// The hardware key mapping is plain Rust: test it on every host.
64#[cfg(all(test, not(gpui_ios)))]
65#[path = "ios/keys.rs"]
66#[allow(dead_code)]
67mod ios_keys;
68#[cfg(all(test, not(gpui_ios)))]
69#[path = "ios/text.rs"]
70#[allow(dead_code)]
71mod ios_text;
72
73/// The active mobile platform layer (keyboard, clipboard, appearance).
74#[cfg(gpui_android)]
75use android as mobile;
76#[cfg(gpui_ios)]
77use ios as mobile;
78
79pub use error::GpuiError;
80pub use gpui;
81#[cfg(all(gpui_backend, not(gpui_mobile)))]
82#[cfg_attr(docsrs, doc(cfg(not(any(target_os = "android", target_os = "ios")))))]
83pub use webview::{WebviewFrame, focus_webview, webview_frame};
84
85use tauri::Wry;
86
87/// Application-wide configuration for the shared GPUI runtime.
88#[derive(Default)]
89pub struct GpuiConfig {
90    font_fallback: Option<String>,
91    on_launch: Option<Box<dyn FnOnce(&mut gpui::App)>>,
92    configure: Option<Box<dyn FnOnce(gpui::Application) -> gpui::Application>>,
93}
94
95impl GpuiConfig {
96    /// Creates a config with default settings.
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Font family used when a requested font is unavailable.
102    pub fn font_fallback(mut self, family: impl Into<String>) -> Self {
103        self.font_fallback = Some(family.into());
104        self
105    }
106
107    /// Runs once with the shared GPUI `App` right after it is created. Use it
108    /// for app-level GPUI setup: key bindings, globals, fonts.
109    pub fn on_launch(mut self, f: impl FnOnce(&mut gpui::App) + 'static) -> Self {
110        self.on_launch = Some(Box::new(f));
111        self
112    }
113
114    /// Sets the GPUI asset source (SVG icons, images).
115    pub fn assets(mut self, assets: impl gpui::AssetSource) -> Self {
116        self.configure = Some(Box::new(move |app| app.with_assets(assets)));
117        self
118    }
119}
120
121/// Per-window attachment options.
122#[derive(Clone, Debug)]
123#[non_exhaustive]
124pub struct GpuiOptions {
125    /// Focus the window once GPUI is attached.
126    pub focus: bool,
127    /// Render with an alpha channel, so pixels GPUI leaves unpainted show
128    /// what is behind the window.
129    pub transparent: bool,
130}
131
132impl Default for GpuiOptions {
133    fn default() -> Self {
134        Self {
135            focus: true,
136            transparent: false,
137        }
138    }
139}
140
141impl GpuiOptions {
142    /// Sets whether the window takes focus once GPUI is attached (default `true`).
143    pub fn focus(mut self, focus: bool) -> Self {
144        self.focus = focus;
145        self
146    }
147
148    /// Sets whether GPUI renders with an alpha channel (default `false`).
149    /// The Tauri window must also be built with `transparent(true)`, and
150    /// the desktop must composite windows for it to show through.
151    pub fn transparent(mut self, transparent: bool) -> Self {
152        self.transparent = transparent;
153        self
154    }
155}
156
157/// Initializes the shared GPUI runtime and hooks it into Tauri's event loop.
158///
159/// Call once from [`tauri::Builder::setup`]. The plugin needs raw TAO events,
160/// which Tauri only exposes through [`tauri::App::wry_plugin`], so it is
161/// registered here rather than through `Builder::plugin`.
162pub fn init(app: &mut tauri::App<Wry>) -> Result<(), GpuiError> {
163    init_with(app, GpuiConfig::default())
164}
165
166/// [`init`] with explicit configuration.
167pub fn init_with(app: &mut tauri::App<Wry>, config: GpuiConfig) -> Result<(), GpuiError> {
168    #[cfg(gpui_backend)]
169    {
170        runtime::initialize(app.handle().clone(), config)?;
171        app.wry_plugin(hook::GpuiHookBuilder);
172        #[cfg(gpui_android)]
173        app.handle().plugin(android_plugin())?;
174        Ok(())
175    }
176    #[cfg(not(gpui_backend))]
177    {
178        let _ = (app, config);
179        Err(GpuiError::UnsupportedOperation {
180            operation: "tauri-plugin-gpui on mobile without the `mobile` feature",
181        })
182    }
183}
184
185/// Registers the `GpuiView` natives, then lets Tauri instantiate the Kotlin
186/// `GpuiPlugin`, which installs the view as the activity's content.
187#[cfg(gpui_android)]
188fn android_plugin() -> tauri::plugin::TauriPlugin<Wry> {
189    tauri::plugin::Builder::new("gpui")
190        .setup(|app, api| {
191            let (tx, rx) = std::sync::mpsc::channel();
192            let _ = app;
193            tauri_runtime_wry::wry::prelude::dispatch(move |env, activity, _webview| {
194                tx.send(android::register_natives(env, activity)).ok();
195            });
196            rx.recv()
197                .map_err(|e| e.to_string())?
198                .map_err(|e| format!("registering GpuiView natives: {e}"))?;
199            api.register_android_plugin("app.tauri.gpui", "GpuiPlugin")?;
200            Ok(())
201        })
202        .build()
203}
204
205/// Runs `f` with the shared GPUI `App`.
206///
207/// Must be called on the main thread and not from inside GPUI code (which
208/// already has a `&mut App`); returns [`GpuiError::Reentrant`] in that case.
209pub fn with_app<R>(f: impl FnOnce(&mut gpui::App) -> R) -> Result<R, GpuiError> {
210    #[cfg(gpui_backend)]
211    {
212        runtime::with(|runtime| runtime.update(f))
213    }
214    #[cfg(not(gpui_backend))]
215    {
216        let _ = f;
217        Err(GpuiError::NotInitialized)
218    }
219}
220
221thread_local! {
222    static BACK_HANDLER: std::cell::RefCell<Option<std::rc::Rc<dyn Fn(&mut gpui::App)>>> =
223        const { std::cell::RefCell::new(None) };
224}
225
226/// Sets the handler for the system back action (the Android back button
227/// or gesture). It only runs while [`set_back_enabled`] is `true`; otherwise
228/// back leaves the app as usual. Never called on desktop.
229///
230/// Call on the main thread (for example from GPUI code).
231pub fn on_back(handler: impl Fn(&mut gpui::App) + 'static) {
232    BACK_HANDLER.with(|slot| *slot.borrow_mut() = Some(std::rc::Rc::new(handler)));
233}
234
235/// Declares whether the app currently handles the back action, typically
236/// whether its navigation stack is deeper than its root. No-op on desktop.
237pub fn set_back_enabled(enabled: bool) {
238    #[cfg(gpui_android)]
239    android::set_back_enabled(enabled);
240    #[cfg(not(gpui_android))]
241    let _ = enabled;
242}
243
244#[cfg_attr(not(gpui_android), allow(dead_code))]
245pub(crate) fn back_handler() -> Option<std::rc::Rc<dyn Fn(&mut gpui::App)>> {
246    BACK_HANDLER.with(|slot| slot.borrow().clone())
247}
248
249/// Attaches GPUI as the content renderer of a Tauri window.
250pub trait GpuiWindowExt {
251    /// Permanently attaches GPUI to this window and mounts `root` as its root
252    /// view. There is no detach: the GPUI root and its rendering surface are
253    /// destroyed together with the Tauri window.
254    ///
255    /// Must be called on the main thread for a window without a WebView.
256    /// Child webviews can be added afterwards and placed with
257    /// [`webview_frame`].
258    /// When called from inside GPUI code the mount completes right after the
259    /// current GPUI update returns.
260    fn attach_gpui<F, V>(&self, root: F) -> Result<(), GpuiError>
261    where
262        F: FnOnce(&mut gpui::App) -> gpui::Entity<V> + 'static,
263        V: gpui::Render + 'static,
264    {
265        self.attach_gpui_with(GpuiOptions::default(), root)
266    }
267
268    /// [`attach_gpui`](Self::attach_gpui) with explicit options.
269    fn attach_gpui_with<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
270    where
271        F: FnOnce(&mut gpui::App) -> gpui::Entity<V> + 'static,
272        V: gpui::Render + 'static,
273    {
274        self.attach_gpui_view(options, move |_, cx| root(cx))
275    }
276
277    /// Like [`attach_gpui_with`](Self::attach_gpui_with), but the root
278    /// builder also receives the GPUI [`Window`](gpui::Window). Component
279    /// libraries need this to wrap content in their root view, e.g.
280    /// `|window, cx| cx.new(|cx| Root::new(view, window, cx))`.
281    fn attach_gpui_view<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
282    where
283        F: FnOnce(&mut gpui::Window, &mut gpui::App) -> gpui::Entity<V> + 'static,
284        V: gpui::Render + 'static;
285
286    /// Whether GPUI is attached to this window.
287    fn is_gpui_attached(&self) -> bool;
288}
289
290impl GpuiWindowExt for tauri::Window<Wry> {
291    fn attach_gpui_view<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
292    where
293        F: FnOnce(&mut gpui::Window, &mut gpui::App) -> gpui::Entity<V> + 'static,
294        V: gpui::Render + 'static,
295    {
296        #[cfg(gpui_backend)]
297        {
298            runtime::with(|runtime| {
299                runtime.attach(
300                    self,
301                    options,
302                    Box::new(move |cx, window_options| {
303                        cx.open_window(window_options, root).map(Into::into)
304                    }),
305                )
306            })
307        }
308        #[cfg(not(gpui_backend))]
309        {
310            let _ = (options, root);
311            Err(GpuiError::UnsupportedOperation {
312                operation: "attach_gpui on mobile",
313            })
314        }
315    }
316
317    fn is_gpui_attached(&self) -> bool {
318        #[cfg(gpui_backend)]
319        {
320            runtime::with(|runtime| Ok(runtime.is_attached(self.label()))).unwrap_or(false)
321        }
322        #[cfg(not(gpui_backend))]
323        {
324            false
325        }
326    }
327}
328
329#[cfg(gpui_backend)]
330mod hook {
331    use tauri::EventLoopMessage;
332    use tauri_runtime_wry::{
333        Context, EventLoopIterationContext, Message, Plugin, PluginBuilder, WebContextStore,
334        tao::{
335            event::Event,
336            event_loop::{ControlFlow, EventLoopProxy, EventLoopWindowTarget},
337        },
338    };
339
340    /// Taps Tauri's TAO event loop. All state lives in the main-thread
341    /// runtime, so the hook itself is stateless.
342    pub(crate) struct GpuiHookBuilder;
343    pub(crate) struct GpuiHook;
344
345    impl PluginBuilder<EventLoopMessage> for GpuiHookBuilder {
346        type Plugin = GpuiHook;
347
348        fn build(self, _context: Context<EventLoopMessage>) -> Self::Plugin {
349            GpuiHook
350        }
351    }
352
353    impl Plugin<EventLoopMessage> for GpuiHook {
354        fn on_event(
355            &mut self,
356            event: &Event<Message<EventLoopMessage>>,
357            _event_loop: &EventLoopWindowTarget<Message<EventLoopMessage>>,
358            proxy: &EventLoopProxy<Message<EventLoopMessage>>,
359            _control_flow: &mut ControlFlow,
360            context: EventLoopIterationContext<'_, EventLoopMessage>,
361            _web_context: &WebContextStore,
362        ) -> bool {
363            // Events are only swallowed when GPUI vetoes a close request;
364            // Tauri handles every other event as usual.
365            crate::runtime::with(|runtime| Ok(runtime.handle_event(event, proxy, &context)))
366                .unwrap_or(false)
367        }
368    }
369}