#[cfg(gpui_android)]
mod android;
#[cfg(gpui_ios)]
mod ios;
#[cfg(all(any(target_os = "linux", target_os = "freebsd"), not(gpui_mobile)))]
mod x11;
use std::{
cell::{Cell, RefCell},
collections::{HashMap, VecDeque},
rc::Rc,
sync::{Arc, Mutex},
thread::{self, ThreadId},
time::{Duration, Instant},
};
use gpui::{
AnyWindowHandle, App, Application, ApplicationHandle, AsyncApp, Bounds, QuitMode,
TitlebarOptions, WindowBounds, WindowOptions,
};
#[cfg(not(gpui_android))]
use gpui::{point, px};
use gpui_wgpu::{CosmicTextSystem, WgpuRenderer, WgpuSurfaceConfig};
#[cfg(not(gpui_android))]
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use tauri::{AppHandle, EventLoopMessage, Wry};
use tauri_runtime_wry::{
EventLoopIterationContext, Message, TaoWindowId, WindowMessage,
tao::{
event::{Event, WindowEvent},
event_loop::EventLoopProxy,
},
};
use crate::{
GpuiConfig, GpuiError, GpuiOptions, events,
ffi::lock,
platform::{
PENDING_SURFACE, TauriPlatform,
dispatcher::{LoopWaker, TauriDispatcher},
window::{RawWindow, WindowInner, default_window_state, logical},
},
};
const TASK_BUDGET: Duration = Duration::from_millis(8);
type OpenWindow = Box<dyn FnOnce(&mut App, WindowOptions) -> anyhow::Result<AnyWindowHandle>>;
pub(crate) struct SurfaceParams {
raw: RawWindow,
physical_size: gpui::Size<gpui::DevicePixels>,
scale_factor: f32,
origin: gpui::Point<gpui::Pixels>,
appearance: gpui::WindowAppearance,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11: Option<crate::platform::x11::X11Window>,
}
struct Mount {
inner: Rc<WindowInner>,
options: WindowOptions,
open: OpenWindow,
}
pub(crate) struct Runtime {
main_thread: ThreadId,
_app: ApplicationHandle,
cx: AsyncApp,
platform: Rc<TauriPlatform>,
dispatcher: Arc<TauriDispatcher>,
waker: Arc<LoopWaker>,
surfaces: RefCell<HashMap<String, Rc<WindowInner>>>,
tao_labels: RefCell<HashMap<TaoWindowId, String>>,
mounts: RefCell<VecDeque<Mount>>,
deferred: Rc<RefCell<Vec<Box<dyn FnOnce()>>>>,
depth: Cell<usize>,
proxy_installed: Cell<bool>,
#[cfg(gpui_android)]
android: android::AndroidState,
#[cfg(gpui_ios)]
ios: ios::IosState,
}
thread_local! {
static RUNTIME: Cell<Option<&'static Runtime>> = const { Cell::new(None) };
}
pub(crate) fn with<R>(
f: impl FnOnce(&'static Runtime) -> Result<R, GpuiError>,
) -> Result<R, GpuiError> {
let runtime = RUNTIME.try_with(Cell::get).ok().flatten();
match runtime {
Some(runtime) => f(runtime),
None if lock(&INITIALIZED).is_some() => Err(GpuiError::NotMainThread),
None => Err(GpuiError::NotInitialized),
}
}
static INITIALIZED: Mutex<Option<ThreadId>> = Mutex::new(None);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) fn input<R>(f: impl FnOnce() -> R) -> Option<R> {
with(|runtime| {
Ok((runtime.depth.get() == 0).then(|| {
let _guard = runtime.enter();
f()
}))
})
.ok()
.flatten()
}
struct DepthGuard<'a>(&'a Cell<usize>);
impl Drop for DepthGuard<'_> {
fn drop(&mut self) {
self.0.set(self.0.get() - 1);
}
}
pub(crate) fn initialize(app: AppHandle<Wry>, config: GpuiConfig) -> Result<(), GpuiError> {
{
let mut initialized = lock(&INITIALIZED);
if initialized.is_some() {
return Err(GpuiError::AlreadyInitialized);
}
*initialized = Some(thread::current().id());
}
let waker = Arc::new(LoopWaker::new(Box::new({
let app = app.clone();
move || {
let _ = app.run_on_main_thread(|| {});
}
})));
let dispatcher = Arc::new(TauriDispatcher::new(waker.clone()));
let text_system = new_text_system(config.font_fallback.as_deref().unwrap_or(default_font()));
let platform = Rc::new(TauriPlatform::new(app, dispatcher.clone(), text_system));
let mut application =
Application::with_platform(platform.clone()).with_quit_mode(QuitMode::Explicit);
if let Some(configure) = config.configure {
application = configure(application);
}
let app = application.run_embedded(|_| {});
let runtime = Box::leak(Box::new(Runtime {
main_thread: thread::current().id(),
cx: app.to_async(),
_app: app,
platform,
dispatcher,
waker,
surfaces: RefCell::default(),
tao_labels: RefCell::default(),
mounts: RefCell::default(),
deferred: Rc::default(),
depth: Cell::new(0),
proxy_installed: Cell::new(false),
#[cfg(gpui_android)]
android: android::AndroidState::default(),
#[cfg(gpui_ios)]
ios: ios::IosState::default(),
}));
#[cfg(gpui_mobile)]
crate::mobile::set_waker(runtime.waker.clone());
RUNTIME.with(|slot| slot.set(Some(runtime)));
if let Some(on_launch) = config.on_launch {
runtime.update(on_launch)?;
}
Ok(())
}
fn is_dummy(window_id: &TaoWindowId) -> bool {
*window_id == unsafe { TaoWindowId::dummy() }
}
#[cfg(not(gpui_mobile))]
fn new_text_system(fallback: &str) -> Arc<CosmicTextSystem> {
Arc::new(CosmicTextSystem::new(fallback))
}
#[cfg(gpui_mobile)]
fn new_text_system(fallback: &str) -> Arc<CosmicTextSystem> {
let text_system = Arc::new(CosmicTextSystem::new_without_system_fonts(fallback));
#[cfg(gpui_android)]
android::load_system_fonts(&*text_system);
#[cfg(gpui_ios)]
ios::load_system_fonts(&*text_system);
text_system
}
fn default_font() -> &'static str {
if cfg!(target_os = "android") {
"Roboto"
} else if cfg!(any(target_os = "macos", target_os = "ios")) {
"Helvetica Neue"
} else if cfg!(target_os = "windows") {
"Segoe UI"
} else {
"DejaVu Sans"
}
}
impl Runtime {
fn enter(&self) -> DepthGuard<'_> {
self.depth.set(self.depth.get() + 1);
DepthGuard(&self.depth)
}
fn is_main_thread(&self) -> bool {
thread::current().id() == self.main_thread
}
pub(crate) fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> Result<R, GpuiError> {
if !self.is_main_thread() {
return Err(GpuiError::NotMainThread);
}
if self.depth.get() > 0 {
return Err(GpuiError::Reentrant);
}
let _guard = self.enter();
Ok(self.cx.update(f))
}
pub(crate) fn attach(
&self,
window: &tauri::Window<Wry>,
options: GpuiOptions,
open: OpenWindow,
) -> Result<(), GpuiError> {
if !self.is_main_thread() {
return Err(GpuiError::NotMainThread);
}
let label = window.label().to_string();
if self.surfaces.borrow().contains_key(&label) {
return Err(GpuiError::AlreadyAttached { label });
}
if !window.webviews().is_empty() {
return Err(GpuiError::NotEligible {
label,
reason: "the window hosts a WebView; GPUI and WebView content cannot share a window",
});
}
#[cfg(gpui_android)]
{
self.attach_android(window, options, open)
}
#[cfg(gpui_ios)]
{
self.attach_ios(window, options, open)
}
#[cfg(not(gpui_mobile))]
{
#[allow(unused_mut)]
let mut params = Self::desktop_surface(window)?;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
self.x11_surface(window, &mut params);
self.attach_surface(window, options, open, params)
}
}
#[cfg(not(gpui_android))]
fn desktop_surface(window: &tauri::Window<Wry>) -> Result<SurfaceParams, GpuiError> {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Ok(gtk_window) = window.gtk_window() {
use gtk::prelude::WidgetExt;
if !gtk_window.is_realized() {
gtk_window.realize();
}
}
let scale_factor = window.scale_factor()? as f32;
let physical = if cfg!(gpui_ios) {
window.outer_size()?
} else {
window.inner_size()?
};
let physical_size = events::physical_size(physical.width.max(1), physical.height.max(1));
let origin = if cfg!(gpui_ios) {
point(px(0.), px(0.))
} else {
window
.outer_position()
.map(|p| {
let p = p.to_logical::<f32>(scale_factor as f64);
point(px(p.x), px(p.y))
})
.unwrap_or_default()
};
let appearance = window
.theme()
.map_or(gpui::WindowAppearance::Light, crate::platform::appearance);
let raw = RawWindow {
window: window
.window_handle()
.map_err(|e| GpuiError::PlatformInitialization(format!("window handle: {e}")))?
.as_raw(),
display: window
.display_handle()
.map_err(|e| GpuiError::PlatformInitialization(format!("display handle: {e}")))?
.as_raw(),
};
Ok(SurfaceParams {
raw,
physical_size,
scale_factor,
origin,
appearance,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11: None,
})
}
fn attach_surface(
&self,
window: &tauri::Window<Wry>,
options: GpuiOptions,
open: OpenWindow,
params: SurfaceParams,
) -> Result<(), GpuiError> {
let SurfaceParams {
raw,
physical_size,
scale_factor,
origin,
appearance,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11,
} = params;
let label = window.label().to_string();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let wayland = crate::platform::wayland::Content::new(raw.window, raw.display);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let raw = match &wayland {
Some(content) => RawWindow {
window: content.window_handle(),
..raw
},
None => raw,
};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let present_mode = wayland
.is_some()
.then_some(gpui_wgpu::wgpu::PresentMode::Mailbox);
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let present_mode = None;
let renderer = WgpuRenderer::new(
self.platform.gpu_context.clone(),
&raw,
WgpuSurfaceConfig {
size: physical_size,
transparent: options.transparent,
preferred_present_mode: present_mode,
},
None,
)
.map_err(|e| GpuiError::RendererInitialization(format!("{e:#}")))?;
let sprite_atlas: Arc<dyn gpui::PlatformAtlas> = renderer.sprite_atlas().clone();
#[cfg(all(feature = "test-support", not(gpui_mobile)))]
let capture = crate::platform::capture::Capture::new(sprite_atlas.clone());
#[cfg(all(feature = "test-support", not(gpui_mobile)))]
let sprite_atlas = capture.as_ref().map_or(sprite_atlas, |c| c.atlas());
let defer: Rc<dyn Fn(Box<dyn FnOnce()>)> = Rc::new({
let deferred = self.deferred.clone();
let waker = self.waker.clone();
move |action| {
deferred.borrow_mut().push(action);
waker.wake();
}
});
let inner = Rc::new_cyclic(|_weak| WindowInner {
label: label.clone(),
tauri_window: window.clone(),
raw: Cell::new(raw),
state: RefCell::new(default_window_state(
physical_size,
scale_factor,
origin,
appearance,
renderer,
)),
callbacks: RefCell::default(),
frame_pending: Cell::new(false),
force_present: Cell::new(false),
mode_dirty: Cell::new(true),
display_dirty: Cell::new(true),
reported_visible: Cell::new(true),
transparent: Cell::new(options.transparent),
sprite_atlas,
#[cfg(all(feature = "test-support", not(gpui_mobile)))]
capture,
closed: Cell::new(false),
gpui_handle: Cell::new(None),
display: RefCell::default(),
waker: self.waker.clone(),
defer,
#[cfg(not(gpui_mobile))]
webviews: RefCell::default(),
#[cfg(not(gpui_mobile))]
file_drop: RefCell::default(),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
ime: crate::platform::ime::linux::Ime::new(window, _weak.clone()),
#[cfg(not(gpui_mobile))]
a11y: RefCell::default(),
#[cfg(not(gpui_mobile))]
a11y_bounds_dirty: Cell::new(true),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
wayland,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11,
});
#[cfg(all(not(gpui_mobile), any(target_os = "linux", target_os = "freebsd")))]
crate::platform::file_drop::gtk::install(window, Rc::downgrade(&inner));
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if inner.wayland.is_some() {
crate::platform::wayland::install(window, Rc::downgrade(&inner));
}
inner.sync_window_mode();
inner.reported_visible.set(inner.state.borrow().visible);
self.surfaces.borrow_mut().insert(label, inner.clone());
let window_options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::new(
origin,
logical(physical_size, scale_factor),
))),
titlebar: Some(TitlebarOptions {
title: window.title().ok().map(Into::into),
..Default::default()
}),
focus: options.focus,
show: false,
..Default::default()
};
let mount = Mount {
inner,
options: window_options,
open,
};
if self.depth.get() == 0 {
self.mount(mount)
} else {
self.mounts.borrow_mut().push_back(mount);
self.waker.wake();
Ok(())
}
}
fn mount(&self, mount: Mount) -> Result<(), GpuiError> {
let Mount {
inner,
options,
open,
} = mount;
if inner.closed.get() || inner.tauri_window.inner_size().is_err() {
self.surfaces.borrow_mut().remove(&inner.label);
inner.destroy();
return Err(GpuiError::NotEligible {
label: inner.label.clone(),
reason: "the window was destroyed before GPUI could be mounted",
});
}
PENDING_SURFACE.with(|pending| *pending.borrow_mut() = Some(inner.clone()));
let result = self.update(|cx| open(cx, options));
PENDING_SURFACE.with(|pending| pending.borrow_mut().take());
let error = match result {
Ok(Ok(handle)) => {
inner.gpui_handle.set(Some(handle));
#[cfg(gpui_ios)]
inner.set_insets(self.ios_insets());
inner.force_present.set(true);
inner.schedule_frame();
return Ok(());
}
Ok(Err(error)) => GpuiError::PlatformInitialization(format!("{error:#}")),
Err(error) => error,
};
self.surfaces.borrow_mut().remove(&inner.label);
inner.destroy();
Err(error)
}
fn surface_for(
&self,
window_id: TaoWindowId,
context: &EventLoopIterationContext<'_, EventLoopMessage>,
) -> Option<Rc<WindowInner>> {
if let Some(label) = self.tao_labels.borrow().get(&window_id) {
return self.surfaces.borrow().get(label).cloned();
}
let id = context.window_id_map.get(&window_id)?;
let windows = context.windows.0.try_borrow().ok()?;
let label = windows.get(&id)?.label().to_string();
drop(windows);
let inner = self.surfaces.borrow().get(&label).cloned()?;
self.tao_labels.borrow_mut().insert(window_id, label);
Some(inner)
}
pub(crate) fn handle_event(
&self,
event: &Event<'_, Message<EventLoopMessage>>,
proxy: &EventLoopProxy<Message<EventLoopMessage>>,
context: &EventLoopIterationContext<'_, EventLoopMessage>,
) -> bool {
if !self.proxy_installed.replace(true) {
let proxy = Mutex::new(proxy.clone());
self.waker.set(Box::new(move || {
let _ = lock(&proxy).send_event(Message::Task(Box::new(|| {})));
}));
}
if self.depth.get() > 0 {
return false;
}
#[cfg(gpui_android)]
if matches!(event, Event::WindowEvent { .. } | Event::RedrawRequested(_)) {
return false;
}
match event {
Event::WindowEvent {
window_id,
event: event @ WindowEvent::ThemeChanged(_),
..
} if is_dummy(window_id) => {
let surfaces: Vec<_> = self.surfaces.borrow().values().cloned().collect();
let _guard = self.enter();
for surface in surfaces {
events::dispatch(&surface, event);
}
}
Event::WindowEvent {
window_id,
event: WindowEvent::CloseRequested,
..
} => {
if let Some(inner) = self.surface_for(*window_id, context) {
return self.veto_close(&inner);
}
}
Event::UserEvent(Message::Window(id, WindowMessage::Close)) => {
let label = context
.windows
.0
.try_borrow()
.ok()
.and_then(|windows| Some(windows.get(id)?.label().to_string()));
let inner = label.and_then(|label| self.surfaces.borrow().get(&label).cloned());
if let Some(inner) = inner {
return self.veto_close(&inner);
}
}
Event::WindowEvent {
window_id, event, ..
} => {
let Some(inner) = self.surface_for(*window_id, context) else {
return false;
};
#[cfg(gpui_ios)]
if self.ios_window_event(&inner, event) {
return false;
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let rewritten;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let event = match self.x11_window_event(&inner, event) {
x11::Rewrite::Keep => event,
x11::Rewrite::Drop => return false,
x11::Rewrite::Replace(event) => {
rewritten = event;
&rewritten
}
};
self.track_window_state(&inner, event);
let destroyed = {
let _guard = self.enter();
events::dispatch(&inner, event)
};
if destroyed {
self.surfaces.borrow_mut().remove(&inner.label);
self.tao_labels.borrow_mut().remove(window_id);
self.forget_platform_refs(&inner);
let _guard = self.enter();
inner.destroy();
}
}
Event::RedrawRequested(window_id) => {
if let Some(inner) = self.surface_for(*window_id, context) {
inner.window_mode_changed();
inner.force_present.set(true);
inner.schedule_frame();
}
}
Event::MainEventsCleared | Event::RedrawEventsCleared => self.drain(),
Event::LoopDestroyed => self.shutdown(),
_ => {}
}
false
}
fn veto_close(&self, inner: &WindowInner) -> bool {
if inner.closed.get() {
return false;
}
let _guard = self.enter();
!inner.should_close()
}
fn track_window_state(&self, inner: &Rc<WindowInner>, event: &WindowEvent<'_>) {
match event {
WindowEvent::CursorEntered { .. } => {
*self.platform.hovered_window.borrow_mut() = Some(inner.tauri_window.clone());
self.platform.cursor.reset();
}
WindowEvent::CursorLeft { .. } => self.unhover(inner),
WindowEvent::Focused(true) => self.platform.active_window.set(inner.gpui_handle.get()),
WindowEvent::Focused(false) => self.deactivate(inner),
_ => {}
}
}
fn unhover(&self, inner: &WindowInner) {
let mut hovered = self.platform.hovered_window.borrow_mut();
if hovered.as_ref().is_some_and(|w| w.label() == inner.label) {
*hovered = None;
}
}
fn deactivate(&self, inner: &WindowInner) {
if self.platform.active_window.get() == inner.gpui_handle.get() {
self.platform.active_window.set(None);
}
}
fn forget_platform_refs(&self, inner: &WindowInner) {
self.unhover(inner);
self.deactivate(inner);
}
fn drain(&self) {
if self.depth.get() > 0 {
return;
}
self.waker.clear();
#[cfg(gpui_android)]
self.pump_android();
#[cfg(gpui_ios)]
self.pump_ios();
let start = Instant::now();
{
let _guard = self.enter();
while let Some(runnable) = self.dispatcher.pop_main() {
runnable.run();
if start.elapsed() > TASK_BUDGET {
break;
}
}
}
loop {
let mount = self.mounts.borrow_mut().pop_front();
let Some(mount) = mount else { break };
if let Err(error) = self.mount(mount) {
log::error!("tauri-plugin-gpui: deferred GPUI attachment failed: {error}");
}
}
let actions = std::mem::take(&mut *self.deferred.borrow_mut());
for action in actions {
action();
}
#[cfg(gpui_ios)]
let frames_allowed = !self.ios.background.get();
#[cfg(not(gpui_ios))]
let frames_allowed = true;
let surfaces: Vec<_> = self.surfaces.borrow().values().cloned().collect();
{
let _guard = self.enter();
for surface in &surfaces {
if surface.closed.get() {
continue;
}
#[cfg(not(gpui_mobile))]
surface.flush_file_drop();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(ime) = &surface.ime {
ime.flush(surface);
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(content) = &surface.wayland {
content.flush();
}
surface.flush_ime_commit();
surface.refresh_window_mode();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
self.sync_x11(surface);
if frames_allowed && surface.reported_visible.get() && surface.frame_pending.take()
{
surface.request_frame();
}
}
}
let frames_pending = frames_allowed
&& surfaces
.iter()
.any(|s| s.reported_visible.get() && s.frame_pending.get());
#[cfg(gpui_android)]
self.sync_text_input();
let more_work = self.dispatcher.has_main_work()
|| !self.mounts.borrow().is_empty()
|| !self.deferred.borrow().is_empty()
|| frames_pending;
if more_work {
self.waker.wake();
}
#[cfg(gpui_ios)]
crate::ios::sync();
}
fn shutdown(&self) {
#[cfg(gpui_ios)]
crate::ios::uninstall();
let surfaces: Vec<_> = self.surfaces.borrow_mut().drain().map(|(_, s)| s).collect();
self.tao_labels.borrow_mut().clear();
let _guard = self.enter();
for surface in surfaces {
self.forget_platform_refs(&surface);
surface.destroy();
}
}
#[cfg(not(gpui_mobile))]
pub(crate) fn surface(&self, label: &str) -> Option<Rc<WindowInner>> {
self.surfaces.try_borrow().ok()?.get(label).cloned()
}
pub(crate) fn is_attached(&self, label: &str) -> bool {
self.surfaces.borrow().contains_key(label)
}
}