284 lines · 9.2 KB · Rust
RawDownload
//! 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<AppCell>,    /// 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<TextSystem>,} impl HeadlessAppContext {    /// Creates a new headless app context with the given text system.    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> 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<dyn PlatformTextSystem>,        asset_source: Arc<dyn AssetSource>,    ) -> 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<dyn PlatformTextSystem>,        asset_source: Arc<dyn AssetSource>,        renderer_factory: impl Fn() -> Option<Box<dyn PlatformHeadlessRenderer>> + '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<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>> =            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<V: Render + 'static>(        &mut self,        size: Size<Pixels>,        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,    ) -> Result<WindowHandle<V>> {        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<R>(&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<R>(        &mut self,        window: AnyWindowHandle,        f: impl FnOnce(AnyView, &mut Window, &mut App) -> R,    ) -> Result<R> {        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<RgbaImage> {        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<TextSystem> {        &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<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {        let mut app = self.app.borrow_mut();        app.new(build_entity)    }     fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {        let mut app = self.app.borrow_mut();        app.reserve_entity()    }     fn insert_entity<T: 'static>(        &mut self,        reservation: Reservation<T>,        build_entity: impl FnOnce(&mut Context<T>) -> T,    ) -> Entity<T> {        let mut app = self.app.borrow_mut();        app.insert_entity(reservation, build_entity)    }     fn update_entity<T: 'static, R>(        &mut self,        handle: &Entity<T>,        update: impl FnOnce(&mut T, &mut Context<T>) -> R,    ) -> R {        let mut app = self.app.borrow_mut();        app.update_entity(handle, update)    }     fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>    where        T: 'static,    {        panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.")    }     fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R    where        T: 'static,    {        let app = self.app.borrow();        app.read_entity(handle, read)    }     fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>    where        F: FnOnce(AnyView, &mut Window, &mut App) -> T,    {        let mut lock = self.app.borrow_mut();        lock.update_window(window, f)    }     fn with_window<R>(        &mut self,        entity_id: EntityId,        f: impl FnOnce(&mut Window, &mut App) -> R,    ) -> Option<R> {        let mut lock = self.app.borrow_mut();        lock.with_window(entity_id, f)    }     fn read_window<T, R>(        &self,        window: &WindowHandle<T>,        read: impl FnOnce(Entity<T>, &App) -> R,    ) -> Result<R>    where        T: 'static,    {        let app = self.app.borrow();        app.read_window(window, read)    }     fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>    where        R: Send + 'static,    {        self.background_executor.spawn(future)    }     fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R    where        G: Global,    {        let app = self.app.borrow();        app.read_global(callback)    }}