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
This commit is contained in:
Jakub Konka authored and GitHub committed 2026-07-03 15:20:09 +00:00
1 parent 5fb0cdfb15
commit 2fff6597d6
3 files changed
+301 -6

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
mod client;
mod window;
pub(crate) use client::*;
+13 -6
View File
@@ -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<calloop::EventLoop<'static, HeadlessClient>>,
pub(crate) common: LinuxCommon,
pub(crate) display: Rc<dyn PlatformDisplay>,
}
#[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<Rc<dyn PlatformDisplay>> {
vec![]
vec![self.0.borrow().display.clone()]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
None
Some(self.0.borrow().display.clone())
}
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
None
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
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<Box<dyn PlatformWindow>> {
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 {
@@ -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<Pixels>,
}
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<Uuid> {
// Stable identity: there is exactly one headless display.
Ok(Uuid::nil())
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
}
struct HeadlessWindowState {
bounds: Bounds<Pixels>,
display: Rc<dyn PlatformDisplay>,
input_handler: Option<PlatformInputHandler>,
title: Option<String>,
is_fullscreen: bool,
}
pub(crate) struct HeadlessWindow(Rc<RefCell<HeadlessWindowState>>);
impl raw_window_handle::HasWindowHandle for HeadlessWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, 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::DisplayHandle<'_>, raw_window_handle::HandleError> {
Err(raw_window_handle::HandleError::NotSupported)
}
}
impl HeadlessWindow {
pub(crate) fn new(params: WindowParams, display: Rc<dyn PlatformDisplay>) -> 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<Pixels> {
self.0.borrow().bounds
}
fn is_maximized(&self) -> bool {
false
}
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Windowed(self.bounds())
}
fn content_size(&self) -> Size<Pixels> {
self.bounds().size
}
fn resize(&mut self, size: Size<Pixels>) {
self.0.borrow_mut().bounds.size = size;
}
fn scale_factor(&self) -> f32 {
1.0
}
fn appearance(&self) -> WindowAppearance {
WindowAppearance::Dark
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.0.borrow().display.clone())
}
fn mouse_position(&self) -> Point<Pixels> {
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<PlatformInputHandler> {
self.0.borrow_mut().input_handler.take()
}
fn prompt(
&self,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
// 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<dyn FnMut(RequestFrameOptions)>) {}
fn on_input(&self, _callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {}
fn on_active_status_change(&self, _callback: Box<dyn FnMut(bool)>) {}
fn on_hover_status_change(&self, _callback: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, _callback: Box<dyn FnMut(Size<Pixels>, f32)>) {}
fn on_moved(&self, _callback: Box<dyn FnMut()>) {}
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {}
fn on_close(&self, _callback: Box<dyn FnOnce()>) {}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
fn draw(&self, _scene: &Scene) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
Arc::new(HeadlessAtlas::default())
}
fn is_subpixel_rendering_supported(&self) -> bool {
false
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
fn gpu_specs(&self) -> Option<GpuSpecs> {
None
}
}
/// Allocates atlas tiles without uploading pixels, so glyph and sprite
/// painting completes headlessly.
#[derive(Default)]
struct HeadlessAtlas(Mutex<HeadlessAtlasState>);
#[derive(Default)]
struct HeadlessAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
impl PlatformAtlas for HeadlessAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
{
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);
}
}