headless_app_context.rs

  1//! Cross-platform headless app context for tests that need real text shaping.
  2//!
  3//! This replaces the macOS-only `HeadlessMetalAppContext` with a platform-neutral
  4//! implementation backed by `TestPlatform`. Tests supply a real `PlatformTextSystem`
  5//! (e.g. `DirectWriteTextSystem` on Windows, `MacTextSystem` on macOS) to get
  6//! accurate glyph measurements while keeping everything else deterministic.
  7//!
  8//! Optionally, a renderer factory can be provided to enable real GPU rendering
  9//! and screenshot capture via [`HeadlessAppContext::capture_screenshot`].
 10
 11use crate::{
 12    AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds,
 13    Context, Entity, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer,
 14    PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem,
 15    Window, WindowBounds, WindowHandle, WindowOptions,
 16    app::{GpuiBorrow, GpuiMode},
 17};
 18use anyhow::Result;
 19use image::RgbaImage;
 20use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
 21
 22/// A cross-platform headless app context for tests that need real text shaping.
 23///
 24/// Unlike the old `HeadlessMetalAppContext`, this works on any platform. It uses
 25/// `TestPlatform` for deterministic scheduling and accepts a pluggable
 26/// `PlatformTextSystem` so tests get real glyph measurements.
 27///
 28/// # Usage
 29///
 30/// ```ignore
 31/// let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new("fallback"));
 32/// let mut cx = HeadlessAppContext::with_platform(
 33///     text_system,
 34///     Arc::new(Assets),
 35///     || gpui_platform::current_headless_renderer(),
 36/// );
 37/// ```
 38pub struct HeadlessAppContext {
 39    /// The underlying app cell.
 40    pub app: Rc<AppCell>,
 41    /// The background executor for running async tasks.
 42    pub background_executor: BackgroundExecutor,
 43    /// The foreground executor for running tasks on the main thread.
 44    pub foreground_executor: ForegroundExecutor,
 45    dispatcher: TestDispatcher,
 46    text_system: Arc<TextSystem>,
 47}
 48
 49impl HeadlessAppContext {
 50    /// Creates a new headless app context with the given text system.
 51    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
 52        Self::with_platform(platform_text_system, Arc::new(()), || None)
 53    }
 54
 55    /// Creates a new headless app context with a custom text system and asset source.
 56    pub fn with_asset_source(
 57        platform_text_system: Arc<dyn PlatformTextSystem>,
 58        asset_source: Arc<dyn AssetSource>,
 59    ) -> Self {
 60        Self::with_platform(platform_text_system, asset_source, || None)
 61    }
 62
 63    /// Creates a new headless app context with the given text system, asset source,
 64    /// and an optional renderer factory for screenshot support.
 65    pub fn with_platform(
 66        platform_text_system: Arc<dyn PlatformTextSystem>,
 67        asset_source: Arc<dyn AssetSource>,
 68        renderer_factory: impl Fn() -> Option<Box<dyn PlatformHeadlessRenderer>> + 'static,
 69    ) -> Self {
 70        let seed = std::env::var("SEED")
 71            .ok()
 72            .and_then(|s| s.parse().ok())
 73            .unwrap_or(0);
 74
 75        let dispatcher = TestDispatcher::new(seed);
 76        let arc_dispatcher = Arc::new(dispatcher.clone());
 77        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
 78        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
 79
 80        let renderer_factory: Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>> =
 81            Box::new(renderer_factory);
 82        let platform = TestPlatform::with_platform(
 83            background_executor.clone(),
 84            foreground_executor.clone(),
 85            platform_text_system.clone(),
 86            Some(renderer_factory),
 87        );
 88
 89        let text_system = Arc::new(TextSystem::new(platform_text_system));
 90        let http_client = http_client::FakeHttpClient::with_404_response();
 91        let app = App::new_app(platform, asset_source, http_client);
 92        app.borrow_mut().mode = GpuiMode::test();
 93
 94        Self {
 95            app,
 96            background_executor,
 97            foreground_executor,
 98            dispatcher,
 99            text_system,
100        }
101    }
102
103    /// Opens a window for headless rendering.
104    pub fn open_window<V: Render + 'static>(
105        &mut self,
106        size: Size<Pixels>,
107        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
108    ) -> Result<WindowHandle<V>> {
109        use crate::{point, px};
110
111        let bounds = Bounds {
112            origin: point(px(0.0), px(0.0)),
113            size,
114        };
115
116        let mut cx = self.app.borrow_mut();
117        cx.open_window(
118            WindowOptions {
119                window_bounds: Some(WindowBounds::Windowed(bounds)),
120                focus: false,
121                show: false,
122                ..Default::default()
123            },
124            build_root,
125        )
126    }
127
128    /// Runs all pending tasks until parked.
129    pub fn run_until_parked(&self) {
130        self.dispatcher.run_until_parked();
131    }
132
133    /// Advances the simulated clock.
134    pub fn advance_clock(&self, duration: Duration) {
135        self.dispatcher.advance_clock(duration);
136    }
137
138    /// Enables parking mode, allowing blocking on real I/O (e.g., async asset loading).
139    pub fn allow_parking(&self) {
140        self.dispatcher.allow_parking();
141    }
142
143    /// Disables parking mode, returning to deterministic test execution.
144    pub fn forbid_parking(&self) {
145        self.dispatcher.forbid_parking();
146    }
147
148    /// Updates app state.
149    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
150        let mut app = self.app.borrow_mut();
151        f(&mut app)
152    }
153
154    /// Updates a window and calls draw to render.
155    pub fn update_window<R>(
156        &mut self,
157        window: AnyWindowHandle,
158        f: impl FnOnce(AnyView, &mut Window, &mut App) -> R,
159    ) -> Result<R> {
160        let mut app = self.app.borrow_mut();
161        app.update_window(window, f)
162    }
163
164    /// Captures a screenshot from a window.
165    ///
166    /// Requires that the context was created with a renderer factory that
167    /// returns `Some` via [`HeadlessAppContext::with_platform`].
168    pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
169        let mut app = self.app.borrow_mut();
170        app.update_window(window, |_, window, _| window.render_to_image())?
171    }
172
173    /// Returns the text system.
174    pub fn text_system(&self) -> &Arc<TextSystem> {
175        &self.text_system
176    }
177
178    /// Returns the background executor.
179    pub fn background_executor(&self) -> &BackgroundExecutor {
180        &self.background_executor
181    }
182
183    /// Returns the foreground executor.
184    pub fn foreground_executor(&self) -> &ForegroundExecutor {
185        &self.foreground_executor
186    }
187}
188
189impl Drop for HeadlessAppContext {
190    fn drop(&mut self) {
191        // Shut down the app so windows are closed and entity handles are
192        // released before the LeakDetector runs.
193        self.app.borrow_mut().shutdown();
194    }
195}
196
197impl AppContext for HeadlessAppContext {
198    fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
199        let mut app = self.app.borrow_mut();
200        app.new(build_entity)
201    }
202
203    fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
204        let mut app = self.app.borrow_mut();
205        app.reserve_entity()
206    }
207
208    fn insert_entity<T: 'static>(
209        &mut self,
210        reservation: Reservation<T>,
211        build_entity: impl FnOnce(&mut Context<T>) -> T,
212    ) -> Entity<T> {
213        let mut app = self.app.borrow_mut();
214        app.insert_entity(reservation, build_entity)
215    }
216
217    fn update_entity<T: 'static, R>(
218        &mut self,
219        handle: &Entity<T>,
220        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
221    ) -> R {
222        let mut app = self.app.borrow_mut();
223        app.update_entity(handle, update)
224    }
225
226    fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
227    where
228        T: 'static,
229    {
230        panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.")
231    }
232
233    fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
234    where
235        T: 'static,
236    {
237        let app = self.app.borrow();
238        app.read_entity(handle, read)
239    }
240
241    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
242    where
243        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
244    {
245        let mut lock = self.app.borrow_mut();
246        lock.update_window(window, f)
247    }
248
249    fn read_window<T, R>(
250        &self,
251        window: &WindowHandle<T>,
252        read: impl FnOnce(Entity<T>, &App) -> R,
253    ) -> Result<R>
254    where
255        T: 'static,
256    {
257        let app = self.app.borrow();
258        app.read_window(window, read)
259    }
260
261    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
262    where
263        R: Send + 'static,
264    {
265        self.background_executor.spawn(future)
266    }
267
268    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
269    where
270        G: Global,
271    {
272        let app = self.app.borrow();
273        app.read_global(callback)
274    }
275}