//! Cross-platform headless app context for tests that need real text shaping. //! //! This replaces the macOS-only `HeadlessMetalAppContext` with a platform-neutral //! implementation backed by `TestPlatform`. Tests supply a real `PlatformTextSystem` //! (e.g. `DirectWriteTextSystem` on Windows, `MacTextSystem` on macOS) to get //! accurate glyph measurements while keeping everything else deterministic. //! //! Optionally, a renderer factory can be provided to enable real GPU rendering //! and screenshot capture via [`HeadlessAppContext::capture_screenshot`]. use crate::{ AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds, Context, Entity, EntityId, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer, PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds, WindowHandle, WindowOptions, app::{GpuiBorrow, GpuiMode}, }; use anyhow::Result; use image::RgbaImage; use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// A cross-platform headless app context for tests that need real text shaping. /// /// Unlike the old `HeadlessMetalAppContext`, this works on any platform. It uses /// `TestPlatform` for deterministic scheduling and accepts a pluggable /// `PlatformTextSystem` so tests get real glyph measurements. /// /// # Usage /// /// ```ignore /// let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new("fallback")); /// let mut cx = HeadlessAppContext::with_platform( /// text_system, /// Arc::new(Assets), /// || gpui_platform::current_headless_renderer(), /// ); /// ``` pub struct HeadlessAppContext { /// The underlying app cell. pub app: Rc, /// The background executor for running async tasks. pub background_executor: BackgroundExecutor, /// The foreground executor for running tasks on the main thread. pub foreground_executor: ForegroundExecutor, dispatcher: TestDispatcher, text_system: Arc, } impl HeadlessAppContext { /// Creates a new headless app context with the given text system. pub fn new(platform_text_system: Arc) -> Self { Self::with_platform(platform_text_system, Arc::new(()), || None) } /// Creates a new headless app context with a custom text system and asset source. pub fn with_asset_source( platform_text_system: Arc, asset_source: Arc, ) -> Self { Self::with_platform(platform_text_system, asset_source, || None) } /// Creates a new headless app context with the given text system, asset source, /// and an optional renderer factory for screenshot support. pub fn with_platform( platform_text_system: Arc, asset_source: Arc, renderer_factory: impl Fn() -> Option> + 'static, ) -> Self { let seed = std::env::var("SEED") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); let dispatcher = TestDispatcher::new(seed); let arc_dispatcher = Arc::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(arc_dispatcher); let renderer_factory: Box Option>> = Box::new(renderer_factory); let platform = TestPlatform::with_platform( background_executor.clone(), foreground_executor.clone(), platform_text_system.clone(), Some(renderer_factory), ); let text_system = Arc::new(TextSystem::new(platform_text_system)); let http_client = http_client::FakeHttpClient::with_404_response(); let app = App::new_app(platform, asset_source, http_client); app.borrow_mut().mode = GpuiMode::test(); Self { app, background_executor, foreground_executor, dispatcher, text_system, } } /// Opens a window for headless rendering. pub fn open_window( &mut self, size: Size, build_root: impl FnOnce(&mut Window, &mut App) -> Entity, ) -> Result> { use crate::{point, px}; let bounds = Bounds { origin: point(px(0.0), px(0.0)), size, }; let mut cx = self.app.borrow_mut(); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), focus: false, show: false, ..Default::default() }, build_root, ) } /// Runs all pending tasks until parked. pub fn run_until_parked(&self) { self.dispatcher.run_until_parked(); } /// Advances the simulated clock. pub fn advance_clock(&self, duration: Duration) { self.dispatcher.advance_clock(duration); } /// Enables parking mode, allowing blocking on real I/O (e.g., async asset loading). pub fn allow_parking(&self) { self.dispatcher.allow_parking(); } /// Disables parking mode, returning to deterministic test execution. pub fn forbid_parking(&self) { self.dispatcher.forbid_parking(); } /// Updates app state. pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { let mut app = self.app.borrow_mut(); f(&mut app) } /// Updates a window and calls draw to render. pub fn update_window( &mut self, window: AnyWindowHandle, f: impl FnOnce(AnyView, &mut Window, &mut App) -> R, ) -> Result { let mut app = self.app.borrow_mut(); app.update_window(window, f) } /// Captures a screenshot from a window. /// /// Requires that the context was created with a renderer factory that /// returns `Some` via [`HeadlessAppContext::with_platform`]. pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { let mut app = self.app.borrow_mut(); app.update_window(window, |_, window, _| window.render_to_image())? } /// Returns the text system. pub fn text_system(&self) -> &Arc { &self.text_system } /// Returns the background executor. pub fn background_executor(&self) -> &BackgroundExecutor { &self.background_executor } /// Returns the foreground executor. pub fn foreground_executor(&self) -> &ForegroundExecutor { &self.foreground_executor } } impl Drop for HeadlessAppContext { fn drop(&mut self) { // Shut down the app so windows are closed and entity handles are // released before the LeakDetector runs. self.app.borrow_mut().shutdown(); } } impl AppContext for HeadlessAppContext { fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { let mut app = self.app.borrow_mut(); app.new(build_entity) } fn reserve_entity(&mut self) -> Reservation { let mut app = self.app.borrow_mut(); app.reserve_entity() } fn insert_entity( &mut self, reservation: Reservation, build_entity: impl FnOnce(&mut Context) -> T, ) -> Entity { let mut app = self.app.borrow_mut(); app.insert_entity(reservation, build_entity) } fn update_entity( &mut self, handle: &Entity, update: impl FnOnce(&mut T, &mut Context) -> R, ) -> R { let mut app = self.app.borrow_mut(); app.update_entity(handle, update) } fn as_mut<'a, T>(&'a mut self, _: &Entity) -> GpuiBorrow<'a, T> where T: 'static, { panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.") } fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R where T: 'static, { let app = self.app.borrow(); app.read_entity(handle, read) } fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut Window, &mut App) -> T, { let mut lock = self.app.borrow_mut(); lock.update_window(window, f) } fn with_window( &mut self, entity_id: EntityId, f: impl FnOnce(&mut Window, &mut App) -> R, ) -> Option { let mut lock = self.app.borrow_mut(); lock.with_window(entity_id, f) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(Entity, &App) -> R, ) -> Result where T: 'static, { let app = self.app.borrow(); app.read_window(window, read) } fn background_spawn(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { self.background_executor.spawn(future) } fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R where G: Global, { let app = self.app.borrow(); app.read_global(callback) } }