#![allow(clippy::type_complexity)]
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod error;
mod ffi;
#[cfg(gpui_backend)]
mod events;
#[cfg(gpui_backend)]
mod platform;
#[cfg(gpui_backend)]
mod runtime;
#[cfg(all(gpui_backend, not(gpui_mobile)))]
mod webview;
#[cfg(gpui_android)]
mod android;
#[cfg(gpui_ios)]
mod ios;
#[cfg(all(test, not(gpui_ios)))]
#[path = "ios/keys.rs"]
#[allow(dead_code)]
mod ios_keys;
#[cfg(all(test, not(gpui_ios)))]
#[path = "ios/text.rs"]
#[allow(dead_code)]
mod ios_text;
#[cfg(gpui_android)]
use android as mobile;
#[cfg(gpui_ios)]
use ios as mobile;
pub use error::GpuiError;
pub use gpui;
#[cfg(all(gpui_backend, not(gpui_mobile)))]
#[cfg_attr(docsrs, doc(cfg(not(any(target_os = "android", target_os = "ios")))))]
pub use webview::{WebviewFrame, focus_webview, webview_frame};
use tauri::Wry;
#[derive(Default)]
pub struct GpuiConfig {
font_fallback: Option<String>,
on_launch: Option<Box<dyn FnOnce(&mut gpui::App)>>,
configure: Option<Box<dyn FnOnce(gpui::Application) -> gpui::Application>>,
}
impl GpuiConfig {
pub fn new() -> Self {
Self::default()
}
pub fn font_fallback(mut self, family: impl Into<String>) -> Self {
self.font_fallback = Some(family.into());
self
}
pub fn on_launch(mut self, f: impl FnOnce(&mut gpui::App) + 'static) -> Self {
self.on_launch = Some(Box::new(f));
self
}
pub fn assets(mut self, assets: impl gpui::AssetSource) -> Self {
self.configure = Some(Box::new(move |app| app.with_assets(assets)));
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct GpuiOptions {
pub focus: bool,
pub transparent: bool,
}
impl Default for GpuiOptions {
fn default() -> Self {
Self {
focus: true,
transparent: false,
}
}
}
impl GpuiOptions {
pub fn focus(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
pub fn transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
}
pub fn init(app: &mut tauri::App<Wry>) -> Result<(), GpuiError> {
init_with(app, GpuiConfig::default())
}
pub fn init_with(app: &mut tauri::App<Wry>, config: GpuiConfig) -> Result<(), GpuiError> {
#[cfg(gpui_backend)]
{
runtime::initialize(app.handle().clone(), config)?;
app.wry_plugin(hook::GpuiHookBuilder);
#[cfg(gpui_android)]
app.handle().plugin(android_plugin())?;
Ok(())
}
#[cfg(not(gpui_backend))]
{
let _ = (app, config);
Err(GpuiError::UnsupportedOperation {
operation: "tauri-plugin-gpui on mobile without the `mobile` feature",
})
}
}
#[cfg(gpui_android)]
fn android_plugin() -> tauri::plugin::TauriPlugin<Wry> {
tauri::plugin::Builder::new("gpui")
.setup(|app, api| {
let (tx, rx) = std::sync::mpsc::channel();
let _ = app;
tauri_runtime_wry::wry::prelude::dispatch(move |env, activity, _webview| {
tx.send(android::register_natives(env, activity)).ok();
});
rx.recv()
.map_err(|e| e.to_string())?
.map_err(|e| format!("registering GpuiView natives: {e}"))?;
api.register_android_plugin("app.tauri.gpui", "GpuiPlugin")?;
Ok(())
})
.build()
}
pub fn with_app<R>(f: impl FnOnce(&mut gpui::App) -> R) -> Result<R, GpuiError> {
#[cfg(gpui_backend)]
{
runtime::with(|runtime| runtime.update(f))
}
#[cfg(not(gpui_backend))]
{
let _ = f;
Err(GpuiError::NotInitialized)
}
}
thread_local! {
static BACK_HANDLER: std::cell::RefCell<Option<std::rc::Rc<dyn Fn(&mut gpui::App)>>> =
const { std::cell::RefCell::new(None) };
}
pub fn on_back(handler: impl Fn(&mut gpui::App) + 'static) {
BACK_HANDLER.with(|slot| *slot.borrow_mut() = Some(std::rc::Rc::new(handler)));
}
pub fn set_back_enabled(enabled: bool) {
#[cfg(gpui_android)]
android::set_back_enabled(enabled);
#[cfg(not(gpui_android))]
let _ = enabled;
}
#[cfg_attr(not(gpui_android), allow(dead_code))]
pub(crate) fn back_handler() -> Option<std::rc::Rc<dyn Fn(&mut gpui::App)>> {
BACK_HANDLER.with(|slot| slot.borrow().clone())
}
pub trait GpuiWindowExt {
fn attach_gpui<F, V>(&self, root: F) -> Result<(), GpuiError>
where
F: FnOnce(&mut gpui::App) -> gpui::Entity<V> + 'static,
V: gpui::Render + 'static,
{
self.attach_gpui_with(GpuiOptions::default(), root)
}
fn attach_gpui_with<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
where
F: FnOnce(&mut gpui::App) -> gpui::Entity<V> + 'static,
V: gpui::Render + 'static,
{
self.attach_gpui_view(options, move |_, cx| root(cx))
}
fn attach_gpui_view<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
where
F: FnOnce(&mut gpui::Window, &mut gpui::App) -> gpui::Entity<V> + 'static,
V: gpui::Render + 'static;
fn is_gpui_attached(&self) -> bool;
}
impl GpuiWindowExt for tauri::Window<Wry> {
fn attach_gpui_view<F, V>(&self, options: GpuiOptions, root: F) -> Result<(), GpuiError>
where
F: FnOnce(&mut gpui::Window, &mut gpui::App) -> gpui::Entity<V> + 'static,
V: gpui::Render + 'static,
{
#[cfg(gpui_backend)]
{
runtime::with(|runtime| {
runtime.attach(
self,
options,
Box::new(move |cx, window_options| {
cx.open_window(window_options, root).map(Into::into)
}),
)
})
}
#[cfg(not(gpui_backend))]
{
let _ = (options, root);
Err(GpuiError::UnsupportedOperation {
operation: "attach_gpui on mobile",
})
}
}
fn is_gpui_attached(&self) -> bool {
#[cfg(gpui_backend)]
{
runtime::with(|runtime| Ok(runtime.is_attached(self.label()))).unwrap_or(false)
}
#[cfg(not(gpui_backend))]
{
false
}
}
}
#[cfg(gpui_backend)]
mod hook {
use tauri::EventLoopMessage;
use tauri_runtime_wry::{
Context, EventLoopIterationContext, Message, Plugin, PluginBuilder, WebContextStore,
tao::{
event::Event,
event_loop::{ControlFlow, EventLoopProxy, EventLoopWindowTarget},
},
};
pub(crate) struct GpuiHookBuilder;
pub(crate) struct GpuiHook;
impl PluginBuilder<EventLoopMessage> for GpuiHookBuilder {
type Plugin = GpuiHook;
fn build(self, _context: Context<EventLoopMessage>) -> Self::Plugin {
GpuiHook
}
}
impl Plugin<EventLoopMessage> for GpuiHook {
fn on_event(
&mut self,
event: &Event<Message<EventLoopMessage>>,
_event_loop: &EventLoopWindowTarget<Message<EventLoopMessage>>,
proxy: &EventLoopProxy<Message<EventLoopMessage>>,
_control_flow: &mut ControlFlow,
context: EventLoopIterationContext<'_, EventLoopMessage>,
_web_context: &WebContextStore,
) -> bool {
crate::runtime::with(|runtime| Ok(runtime.handle_event(event, proxy, &context)))
.unwrap_or(false)
}
}
}