From 2fff6597d69428dc16070f00671c6f38e53a887e Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 3 Jul 2026 17:20:09 +0200 Subject: [PATCH] gpui_linux: Add support for open_window in headless client (#60359) This actually makes it possible for Linux headless client to have working windows for computation (well, we are in headless mode after all). This also matches macOS headless client behaviour. Release Notes: - N/A --- crates/gpui_linux/src/linux/headless.rs | 1 + .../gpui_linux/src/linux/headless/client.rs | 19 +- .../gpui_linux/src/linux/headless/window.rs | 287 ++++++++++++++++++ 3 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 crates/gpui_linux/src/linux/headless/window.rs diff --git a/crates/gpui_linux/src/linux/headless.rs b/crates/gpui_linux/src/linux/headless.rs index 2237aeb194..6667a3b5a1 100644 --- a/crates/gpui_linux/src/linux/headless.rs +++ b/crates/gpui_linux/src/linux/headless.rs @@ -1,3 +1,4 @@ mod client; +mod window; pub(crate) use client::*; diff --git a/crates/gpui_linux/src/linux/headless/client.rs b/crates/gpui_linux/src/linux/headless/client.rs index e127da1f2f..06da3cbd94 100644 --- a/crates/gpui_linux/src/linux/headless/client.rs +++ b/crates/gpui_linux/src/linux/headless/client.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use calloop::{EventLoop, LoopHandle}; use gpui_util::ResultExt; +use crate::linux::headless::window::{HeadlessDisplay, HeadlessWindow}; use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout}; use gpui::{ AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout, @@ -14,6 +15,7 @@ pub struct HeadlessClientState { pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>, pub(crate) event_loop: Option>, pub(crate) common: LinuxCommon, + pub(crate) display: Rc, } #[derive(Clone)] @@ -47,6 +49,7 @@ impl HeadlessClient { event_loop: Some(event_loop), _loop_handle: handle, common, + display: Rc::new(HeadlessDisplay::new()), }))) } } @@ -61,15 +64,16 @@ impl LinuxClient for HeadlessClient { } fn displays(&self) -> Vec> { - vec![] + vec![self.0.borrow().display.clone()] } fn primary_display(&self) -> Option> { - None + Some(self.0.borrow().display.clone()) } - fn display(&self, _id: DisplayId) -> Option> { - None + fn display(&self, id: DisplayId) -> Option> { + let display = self.0.borrow().display.clone(); + (display.id() == id).then_some(display) } #[cfg(feature = "screen-capture")] @@ -96,9 +100,12 @@ impl LinuxClient for HeadlessClient { fn open_window( &self, _handle: AnyWindowHandle, - _params: WindowParams, + params: WindowParams, ) -> anyhow::Result> { - anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode"); + Ok(Box::new(HeadlessWindow::new( + params, + self.0.borrow().display.clone(), + ))) } fn compositor_name(&self) -> &'static str { diff --git a/crates/gpui_linux/src/linux/headless/window.rs b/crates/gpui_linux/src/linux/headless/window.rs new file mode 100644 index 0000000000..e41b09723d --- /dev/null +++ b/crates/gpui_linux/src/linux/headless/window.rs @@ -0,0 +1,287 @@ +//! Windows for the headless platform client. +//! +//! A headless window has no compositor surface and no GPU: layout, text +//! shaping, and entity plumbing run normally, `draw` discards the scene, and +//! the sprite atlas hands out tiles without uploading pixels (mirroring +//! GPUI's `TestWindow`/`TestAtlas`). This lets command-line tools drive real +//! `Window`-based code paths without a display server. + +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +use collections::HashMap; +use parking_lot::Mutex; +use uuid::Uuid; + +use gpui::{ + AtlasKey, AtlasTextureId, AtlasTile, Bounds, Capslock, DevicePixels, DispatchEventResult, + DisplayId, GpuSpecs, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + Scene, Size, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControlArea, WindowParams, px, +}; + +#[derive(Debug)] +pub(crate) struct HeadlessDisplay { + bounds: Bounds, +} + +impl HeadlessDisplay { + pub(crate) fn new() -> Self { + Self { + bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))), + } + } +} + +impl PlatformDisplay for HeadlessDisplay { + fn id(&self) -> DisplayId { + DisplayId::new(0) + } + + fn uuid(&self) -> anyhow::Result { + // Stable identity: there is exactly one headless display. + Ok(Uuid::nil()) + } + + fn bounds(&self) -> Bounds { + self.bounds + } +} + +struct HeadlessWindowState { + bounds: Bounds, + display: Rc, + input_handler: Option, + title: Option, + is_fullscreen: bool, +} + +pub(crate) struct HeadlessWindow(Rc>); + +impl raw_window_handle::HasWindowHandle for HeadlessWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + // Headless windows are not backed by a native window. + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl raw_window_handle::HasDisplayHandle for HeadlessWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl HeadlessWindow { + pub(crate) fn new(params: WindowParams, display: Rc) -> Self { + Self(Rc::new(RefCell::new(HeadlessWindowState { + bounds: params.bounds, + display, + input_handler: None, + title: None, + is_fullscreen: false, + }))) + } +} + +impl PlatformWindow for HeadlessWindow { + fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + + fn is_maximized(&self) -> bool { + false + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn content_size(&self) -> Size { + self.bounds().size + } + + fn resize(&mut self, size: Size) { + self.0.borrow_mut().bounds.size = size; + } + + fn scale_factor(&self) -> f32 { + 1.0 + } + + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + fn display(&self) -> Option> { + Some(self.0.borrow().display.clone()) + } + + fn mouse_position(&self) -> Point { + Point::default() + } + + fn modifiers(&self) -> Modifiers { + Modifiers::default() + } + + fn capslock(&self) -> Capslock { + Capslock::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + // Fall back to GPUI's rendered prompts. + None + } + + fn activate(&self) {} + + fn is_active(&self) -> bool { + false + } + + fn is_hovered(&self) -> bool { + false + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } + + fn set_title(&mut self, title: &str) { + self.0.borrow_mut().title = Some(title.to_owned()); + } + + fn get_title(&self) -> String { + self.0.borrow().title.clone().unwrap_or_default() + } + + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + + fn minimize(&self) {} + + fn zoom(&self) {} + + fn toggle_fullscreen(&self) { + let mut state = self.0.borrow_mut(); + state.is_fullscreen = !state.is_fullscreen; + } + + fn is_fullscreen(&self) -> bool { + self.0.borrow().is_fullscreen + } + + // No compositor drives a frame loop, so frame and status callbacks are + // dropped: anything that awaits a frame will never resolve headlessly. + fn on_request_frame(&self, _callback: Box) {} + + fn on_input(&self, _callback: Box DispatchEventResult>) {} + + fn on_active_status_change(&self, _callback: Box) {} + + fn on_hover_status_change(&self, _callback: Box) {} + + fn on_resize(&self, _callback: Box, f32)>) {} + + fn on_moved(&self, _callback: Box) {} + + fn on_should_close(&self, _callback: Box bool>) {} + + fn on_close(&self, _callback: Box) {} + + fn on_hit_test_window_control(&self, _callback: Box Option>) { + } + + fn on_appearance_changed(&self, _callback: Box) {} + + fn draw(&self, _scene: &Scene) {} + + fn sprite_atlas(&self) -> Arc { + Arc::new(HeadlessAtlas::default()) + } + + fn is_subpixel_rendering_supported(&self) -> bool { + false + } + + fn update_ime_position(&self, _bounds: Bounds) {} + + fn gpu_specs(&self) -> Option { + None + } +} + +/// Allocates atlas tiles without uploading pixels, so glyph and sprite +/// painting completes headlessly. +#[derive(Default)] +struct HeadlessAtlas(Mutex); + +#[derive(Default)] +struct HeadlessAtlasState { + next_id: u32, + tiles: HashMap, +} + +impl PlatformAtlas for HeadlessAtlas { + fn get_or_insert_with<'a>( + &self, + key: &AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result< + Option<(Size, std::borrow::Cow<'a, [u8]>)>, + >, + ) -> anyhow::Result> { + { + let state = self.0.lock(); + if let Some(&tile) = state.tiles.get(key) { + return Ok(Some(tile)); + } + } + + let Some((size, _)) = build()? else { + return Ok(None); + }; + + let mut state = self.0.lock(); + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + let tile = AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: key.texture_kind(), + }, + tile_id: TileId(tile_id), + padding: 0, + bounds: Bounds { + origin: Point::default(), + size, + }, + }; + state.tiles.insert(key.clone(), tile); + Ok(Some(tile)) + } + + fn remove(&self, key: &AtlasKey) { + self.0.lock().tiles.remove(key); + } +}