use std::any::Any;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread::{self, JoinHandle};
use anyhow::{Context as _, anyhow, bail};
use futures::StreamExt as _;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
use gpui::{
AnyWindowHandle, App, Application, AsyncApp, Bounds, DevicePixels, Entity, GpuSpecs,
PixelBuffer, Pixels, PlatformAtlas, PlatformRenderer, Point, Render, RendererFactory,
RendererTarget, Scene, SceneRenderer, Size, Window, WindowBounds, WindowOptions,
};
#[derive(Clone, Copy, Debug)]
pub struct GuestOptions {
pub size: Size<Pixels>,
}
pub struct GuestFrame {
surface: Arc<dyn Any + Send + Sync>,
size: Size<DevicePixels>,
}
impl GuestFrame {
pub fn surface(&self) -> &Arc<dyn Any + Send + Sync> {
&self.surface
}
pub fn size(&self) -> Size<DevicePixels> {
self.size
}
}
type GuestUpdate = Box<dyn FnOnce(&mut Window, &mut App) + Send>;
enum GuestCommand {
Present { size: Option<Size<Pixels>> },
Update(GuestUpdate),
Shutdown,
}
pub struct Guest {
commands: UnboundedSender<GuestCommand>,
frames: Receiver<Option<GuestFrame>>,
thread: Option<JoinHandle<()>>,
}
impl Guest {
pub fn spawn<V, B, R>(options: GuestOptions, renderer: R, build_root: B) -> anyhow::Result<Self>
where
V: Render + 'static,
B: FnOnce(&mut Window, &mut App) -> Entity<V> + Send + 'static,
R: Fn() -> anyhow::Result<Box<dyn PlatformRenderer>> + Send + 'static,
{
let (commands, command_queue) = unbounded();
let (frame_sender, frames) = channel();
let (ready_sender, ready) = channel();
let thread = thread::Builder::new()
.name("gpui-guest".to_owned())
.spawn(move || {
let application: Application = gpui::headless();
application.run(move |cx| {
let started = open_guest_window(
cx,
options,
renderer,
build_root,
command_queue,
frame_sender,
);
let failed = started.is_err();
let _ = ready_sender.send(started.map_err(|error| format!("{error:#}")));
if failed {
cx.quit();
}
});
})
.context("could not spawn the guest thread")?;
match ready.recv() {
Ok(Ok(())) => Ok(Self {
commands,
frames,
thread: Some(thread),
}),
Ok(Err(error)) => {
thread.join().ok();
bail!("the guest could not start: {error}")
}
Err(_) => {
thread.join().ok();
bail!("the guest thread stopped before it was ready")
}
}
}
pub fn request_frame(&mut self, size: Option<Size<Pixels>>) -> anyhow::Result<GuestFrame> {
while self.frames.try_recv().is_ok() {}
self.commands
.unbounded_send(GuestCommand::Present { size })
.map_err(|_| anyhow!("the guest thread has stopped"))?;
match self.frames.recv() {
Ok(Some(frame)) => Ok(frame),
Ok(None) => bail!("the guest's renderer did not produce a shareable surface"),
Err(_) => bail!("the guest thread stopped before presenting a frame"),
}
}
pub fn update(
&self,
update: impl FnOnce(&mut Window, &mut App) + Send + 'static,
) -> anyhow::Result<()> {
self.commands
.unbounded_send(GuestCommand::Update(Box::new(update)))
.map_err(|_| anyhow!("the guest thread has stopped"))
}
pub fn shutdown(mut self) -> anyhow::Result<()> {
self.stop()
}
fn stop(&mut self) -> anyhow::Result<()> {
let Some(thread) = self.thread.take() else {
return Ok(());
};
let _ = self.commands.unbounded_send(GuestCommand::Shutdown);
thread
.join()
.map_err(|_| anyhow!("the guest thread panicked"))?;
Ok(())
}
}
impl Drop for Guest {
fn drop(&mut self) {
let _ = self.stop();
}
}
fn open_guest_window<V, B, R>(
cx: &mut App,
options: GuestOptions,
renderer: R,
build_root: B,
commands: UnboundedReceiver<GuestCommand>,
frames: Sender<Option<GuestFrame>>,
) -> anyhow::Result<()>
where
V: Render + 'static,
B: FnOnce(&mut Window, &mut App) -> Entity<V>,
R: Fn() -> anyhow::Result<Box<dyn PlatformRenderer>> + 'static,
{
let window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: Point::default(),
size: options.size,
})),
focus: false,
show: false,
..WindowOptions::default()
}
.with_renderer_factory(GuestRendererFactory { renderer, frames }),
build_root,
)?;
let window: AnyWindowHandle = window.into();
cx.spawn(async move |cx| guest_loop(cx, window, commands).await)
.detach();
Ok(())
}
async fn guest_loop(
cx: &mut AsyncApp,
window: AnyWindowHandle,
mut commands: UnboundedReceiver<GuestCommand>,
) {
while let Some(command) = commands.next().await {
match command {
GuestCommand::Present { size } => {
let presented = window.update(cx, |_, window, cx| {
if let Some(size) = size {
window.resize(size);
window.refresh();
}
window.draw(cx).clear(cx);
window.present();
});
if presented.is_err() {
break;
}
}
GuestCommand::Update(update) => {
if window
.update(cx, |_, window, cx| update(window, cx))
.is_err()
{
break;
}
}
GuestCommand::Shutdown => {
cx.update(|cx| cx.quit());
return;
}
}
}
}
struct GuestRendererFactory<R> {
renderer: R,
frames: Sender<Option<GuestFrame>>,
}
impl<R> RendererFactory for GuestRendererFactory<R>
where
R: Fn() -> anyhow::Result<Box<dyn PlatformRenderer>> + 'static,
{
fn create(&self, target: RendererTarget<'_>) -> anyhow::Result<Box<dyn PlatformRenderer>> {
Ok(Box::new(FrameTap {
inner: (self.renderer)()?,
frames: self.frames.clone(),
size: target.size.to_device_pixels(target.scale_factor),
}))
}
}
struct FrameTap {
inner: Box<dyn PlatformRenderer>,
frames: Sender<Option<GuestFrame>>,
size: Size<DevicePixels>,
}
impl SceneRenderer for FrameTap {
fn draw(&mut self, scene: &Scene) -> bool {
let presented = self.inner.draw(scene);
let _ = self.frames.send(
self.inner
.offscreen_surface_handle()
.map(|surface| GuestFrame {
surface,
size: self.size,
}),
);
presented
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.inner.sprite_atlas()
}
fn set_viewport_size(&mut self, size: Size<DevicePixels>) {
self.size = size;
self.inner.set_viewport_size(size);
}
fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> anyhow::Result<()> {
self.size = size;
self.inner.render_scene(scene, size)
}
fn read_pixels(&mut self) -> anyhow::Result<PixelBuffer> {
self.inner.read_pixels()
}
fn offscreen_surface_handle(&self) -> Option<Arc<dyn Any + Send + Sync>> {
self.inner.offscreen_surface_handle()
}
}
#[cfg(target_os = "macos")]
impl gpui::MacSceneRenderer for FrameTap {
fn layer_ptr(&self) -> *mut std::ffi::c_void {
self.inner.layer_ptr()
}
fn set_presents_with_transaction(&mut self, value: bool) {
self.inner.set_presents_with_transaction(value);
}
}
#[cfg(target_os = "windows")]
impl gpui::WinSceneRenderer for FrameTap {
fn set_background_appearance(&mut self, appearance: gpui::WindowBackgroundAppearance) {
self.inner.set_background_appearance(appearance);
}
fn resize(&mut self, size: Size<DevicePixels>) -> anyhow::Result<()> {
self.inner.resize(size)
}
fn mark_drawable(&mut self) {
self.inner.mark_drawable();
}
}
impl PlatformRenderer for FrameTap {
fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
self.size = size;
self.inner.update_drawable_size(size);
}
fn max_texture_size(&self) -> u32 {
self.inner.max_texture_size()
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
self.inner.gpu_specs()
}
fn set_subpixel_layout(&mut self, is_bgr: bool) {
self.inner.set_subpixel_layout(is_bgr);
}
fn update_transparency(&mut self, transparent: bool) {
self.inner.update_transparency(transparent);
}
fn destroy(&mut self) {
self.inner.destroy();
}
fn device_lost(&self) -> bool {
self.inner.device_lost()
}
fn needs_redraw(&mut self) -> bool {
self.inner.needs_redraw()
}
fn recover(&mut self, target: RendererTarget<'_>) -> anyhow::Result<()> {
self.inner.recover(target)
}
}