Skip to main content

gpui/app/
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//! to get accurate glyph measurements while keeping everything else deterministic.
6//! (e.g. `DirectWriteTextSystem` on Windows, `AppleTextSystem` on macOS) to get
7//! accurate glyph measurements while keeping everything else deterministic.
8//!
9//! Optionally, a renderer factory can be provided to enable real GPU rendering
10//! and screenshot capture via [`HeadlessAppContext::capture_screenshot`].
11
12use crate::{
13    AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetRegistry, AssetSource,
14    BackgroundExecutor, Bounds, Context, Entity, EntityId, EventEmitter, ForegroundExecutor,
15    Global, Hsla, Pixels, PlatformHeadlessRenderer, PlatformTextSystem, Render, Reservation,
16    ScaledPixels, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
17    WindowHandle, WindowOptions,
18    app::{GpuiBorrow, GpuiMode},
19};
20use anyhow::Result;
21use image::RgbaImage;
22use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
23
24/// A headless app for tests that need real text layout, without a window on screen.
25///
26/// Like [`TestAppContext`](crate::TestAppContext), this runs on a deterministic
27/// scheduler and a test platform. Unlike it, you supply the text system, so text
28/// is measured the way the platform measures it. Windows are hidden and unfocused.
29/// Use [`HeadlessAppContext::debug_bounds`] and the `*_bounds` methods to check
30/// where things were painted, and [`HeadlessAppContext::capture_screenshot`] to
31/// get pixels when a renderer is configured.
32///
33/// The text system comes from a platform or text crate. For example, the
34/// `ParleyTextSystem` in the `gpui_ce_parley` crate measures text with real fonts.
35/// This example uses [`TestTextSystem`](crate::TestTextSystem), which is for tests
36/// that don't check text metrics:
37///
38/// ```
39/// use std::sync::Arc;
40///
41/// use gpui::{AppContext, HeadlessAppContext, TestTextSystem};
42///
43/// let mut cx = HeadlessAppContext::new(Arc::new(TestTextSystem));
44/// let count = cx.new(|_| 1usize);
45/// assert_eq!(cx.read_entity(&count, |count, _| *count), 1);
46/// ```
47pub struct HeadlessAppContext {
48    /// The underlying app cell.
49    pub app: Rc<AppCell>,
50    /// The background executor for running async tasks.
51    pub background_executor: BackgroundExecutor,
52    /// The foreground executor for running tasks on the main thread.
53    pub foreground_executor: ForegroundExecutor,
54    dispatcher: TestDispatcher,
55    text_system: Arc<TextSystem>,
56}
57
58impl HeadlessAppContext {
59    /// Creates a headless context that measures text with `platform_text_system`.
60    ///
61    /// It has no asset source and no renderer, so [`HeadlessAppContext::capture_screenshot`]
62    /// returns an error. Use [`HeadlessAppContext::with_platform`] for screenshots.
63    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
64        Self::with_platform(platform_text_system, Arc::new(()), || Ok(None))
65    }
66
67    /// Creates a headless context with `platform_text_system` and `asset_source`, and no renderer.
68    pub fn with_asset_source(
69        platform_text_system: Arc<dyn PlatformTextSystem>,
70        asset_source: Arc<dyn AssetSource>,
71    ) -> Self {
72        Self::with_platform(platform_text_system, asset_source, || Ok(None))
73    }
74
75    /// Creates a headless context with a text system, an asset source and a renderer factory.
76    ///
77    /// `renderer_factory` creates the renderer that windows draw with. Return `None`
78    /// from it to draw without a renderer, which disables screenshots. The scheduler seed
79    /// is read from `SEED`, and is `0` when it's unset or isn't a number.
80    pub fn with_platform(
81        platform_text_system: Arc<dyn PlatformTextSystem>,
82        asset_source: Arc<dyn AssetSource>,
83        renderer_factory: impl Fn() -> Result<Option<Box<dyn PlatformHeadlessRenderer>>> + 'static,
84    ) -> Self {
85        let seed = std::env::var("SEED")
86            .ok()
87            .and_then(|s| s.parse().ok())
88            .unwrap_or(0);
89
90        let dispatcher = TestDispatcher::new(seed);
91        let arc_dispatcher = Arc::new(dispatcher.clone());
92        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
93        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
94
95        let renderer_factory: Box<dyn Fn() -> Result<Option<Box<dyn PlatformHeadlessRenderer>>>> =
96            Box::new(renderer_factory);
97        let platform = TestPlatform::with_platform(
98            background_executor.clone(),
99            foreground_executor.clone(),
100            platform_text_system.clone(),
101            Some(renderer_factory),
102        );
103
104        let text_system = Arc::new(TextSystem::new(platform_text_system));
105        let http_client = crate::http_client::FakeHttpClient::with_404_response();
106        let app = App::new_app(
107            platform,
108            AssetRegistry::from(asset_source).into(),
109            http_client,
110        );
111        app.borrow_mut().mode = GpuiMode::test();
112
113        Self {
114            app,
115            background_executor,
116            foreground_executor,
117            dispatcher,
118            text_system,
119        }
120    }
121
122    /// Opens a hidden, unfocused window of `size` at the origin, with a root view from `build_root`.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the window can't be opened.
127    pub fn open_window<V: Render + 'static>(
128        &mut self,
129        size: Size<Pixels>,
130        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
131    ) -> Result<WindowHandle<V>> {
132        use crate::{point, px};
133
134        let bounds = Bounds {
135            origin: point(px(0.0), px(0.0)),
136            size,
137        };
138
139        let mut cx = self.app.borrow_mut();
140        cx.open_window(
141            WindowOptions::new()
142                .window_bounds(Some(WindowBounds::Windowed(bounds)))
143                .focus(false)
144                .show(false),
145            build_root,
146        )
147    }
148
149    /// Runs pending tasks until none can make progress. This doesn't move the simulated clock.
150    pub fn run_until_parked(&self) {
151        self.dispatcher.run_until_parked();
152    }
153
154    /// Moves the simulated clock forward by `duration`, and runs the tasks that become ready.
155    pub fn advance_clock(&self, duration: Duration) {
156        self.dispatcher.advance_clock(duration);
157    }
158
159    /// Lets blocking waits in the scheduler sleep in real time, for tests that wait on real I/O.
160    ///
161    /// Pair it with [`HeadlessAppContext::forbid_parking`]. See
162    /// [`BackgroundExecutor::allow_parking`](crate::BackgroundExecutor::allow_parking).
163    pub fn allow_parking(&self) {
164        self.dispatcher.allow_parking();
165    }
166
167    /// Turns off the parking mode from [`HeadlessAppContext::allow_parking`], returning to deterministic execution.
168    pub fn forbid_parking(&self) {
169        self.dispatcher.forbid_parking();
170    }
171
172    /// Runs `f` with mutable access to the [`App`]. This doesn't run pending tasks.
173    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
174        let mut app = self.app.borrow_mut();
175        f(&mut app)
176    }
177
178    /// Runs `f` with the window's root view, the [`Window`] and the [`App`].
179    ///
180    /// This doesn't paint the window. Methods that read painted output, such as
181    /// [`HeadlessAppContext::debug_bounds`], see the last frame that was painted.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if `window` has been closed.
186    pub fn update_window<R>(
187        &mut self,
188        window: AnyWindowHandle,
189        f: impl FnOnce(AnyView, &mut Window, &mut App) -> R,
190    ) -> Result<R> {
191        let mut app = self.app.borrow_mut();
192        app.update_window(window, f)
193    }
194
195    /// Returns the bounds of the element with `debug_selector(selector)` in the last painted frame, or `None`.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if `window` has been closed.
200    pub fn debug_bounds(
201        &mut self,
202        window: AnyWindowHandle,
203        selector: &str,
204    ) -> Result<Option<Bounds<Pixels>>> {
205        self.update_window(window, |_, window, _| {
206            window.rendered_frame.debug_bounds.get(selector).copied()
207        })
208    }
209
210    /// Returns the device-pixel bounds of each solid quad in `color` in the last painted frame.
211    ///
212    /// The color must match exactly. Quads with other colors are skipped.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if `window` has been closed.
217    pub fn solid_quad_bounds(
218        &mut self,
219        window: AnyWindowHandle,
220        color: Hsla,
221    ) -> Result<Vec<Bounds<ScaledPixels>>> {
222        let color = color.into();
223        self.update_window(window, |_, window, _| {
224            window
225                .rendered_frame
226                .scene
227                .quads
228                .iter()
229                .filter(|quad| quad.background.solid == color)
230                .map(|quad| quad.bounds)
231                .collect()
232        })
233    }
234
235    /// Returns the device-pixel bounds of each monochrome glyph painted in `color` in the last painted frame.
236    ///
237    /// The color must match exactly.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if `window` has been closed.
242    pub fn glyph_bounds(
243        &mut self,
244        window: AnyWindowHandle,
245        color: Hsla,
246    ) -> Result<Vec<Bounds<ScaledPixels>>> {
247        let color = color.into();
248        self.update_window(window, |_, window, _| {
249            window
250                .rendered_frame
251                .scene
252                .monochrome_sprites
253                .iter()
254                .filter(|sprite| sprite.color == color)
255                .map(|sprite| sprite.bounds)
256                .collect()
257        })
258    }
259
260    /// Returns the device-pixel bounds of each underline painted in `color` in the last painted frame.
261    ///
262    /// The color must match exactly.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if `window` has been closed.
267    pub fn underline_bounds(
268        &mut self,
269        window: AnyWindowHandle,
270        color: Hsla,
271    ) -> Result<Vec<Bounds<ScaledPixels>>> {
272        let color = color.into();
273        self.update_window(window, |_, window, _| {
274            window
275                .rendered_frame
276                .scene
277                .underlines
278                .iter()
279                .filter(|underline| underline.color == color)
280                .map(|underline| underline.bounds)
281                .collect()
282        })
283    }
284
285    /// Renders `window`'s last painted frame to an image.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the context was created without a renderer, which is the
290    /// case for [`HeadlessAppContext::new`] and [`HeadlessAppContext::with_asset_source`].
291    /// Also returns an error if the window has been closed.
292    pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
293        let mut app = self.app.borrow_mut();
294        app.update_window(window, |_, window, _| window.render_to_image())?
295    }
296
297    /// Returns the text system.
298    pub fn text_system(&self) -> &Arc<TextSystem> {
299        &self.text_system
300    }
301
302    /// Returns the background executor.
303    pub fn background_executor(&self) -> &BackgroundExecutor {
304        &self.background_executor
305    }
306
307    /// Returns the foreground executor.
308    pub fn foreground_executor(&self) -> &ForegroundExecutor {
309        &self.foreground_executor
310    }
311}
312
313impl Drop for HeadlessAppContext {
314    fn drop(&mut self) {
315        // Shut down the app so windows are closed and entity handles are
316        // released before the LeakDetector runs.
317        self.app.borrow_mut().shutdown();
318    }
319}
320
321impl AppContext for HeadlessAppContext {
322    fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
323        let mut app = self.app.borrow_mut();
324        app.new(build_entity)
325    }
326
327    fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
328        let mut app = self.app.borrow_mut();
329        app.reserve_entity()
330    }
331
332    fn insert_entity<T: 'static>(
333        &mut self,
334        reservation: Reservation<T>,
335        build_entity: impl FnOnce(&mut Context<T>) -> T,
336    ) -> Entity<T> {
337        let mut app = self.app.borrow_mut();
338        app.insert_entity(reservation, build_entity)
339    }
340
341    fn update_entity<T: 'static, R>(
342        &mut self,
343        handle: &Entity<T>,
344        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
345    ) -> R {
346        let mut app = self.app.borrow_mut();
347        app.update_entity(handle, update)
348    }
349
350    fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
351    where
352        T: 'static,
353    {
354        panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.")
355    }
356
357    fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
358    where
359        T: 'static,
360    {
361        let app = self.app.borrow();
362        app.read_entity(handle, read)
363    }
364
365    fn notify(&mut self, entity_id: EntityId) {
366        let mut app = self.app.borrow_mut();
367        app.notify(entity_id);
368    }
369
370    fn emit<EntityType, EventType>(&mut self, entity: &Entity<EntityType>, event: EventType)
371    where
372        EntityType: EventEmitter<EventType>,
373        EventType: 'static,
374    {
375        let mut app = self.app.borrow_mut();
376        app.emit(entity, event)
377    }
378
379    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
380    where
381        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
382    {
383        let mut lock = self.app.borrow_mut();
384        lock.update_window(window, f)
385    }
386
387    fn with_window<R>(
388        &mut self,
389        entity_id: EntityId,
390        f: impl FnOnce(&mut Window, &mut App) -> R,
391    ) -> Option<R> {
392        let mut lock = self.app.borrow_mut();
393        lock.with_window(entity_id, f)
394    }
395
396    fn read_window<T, R>(
397        &self,
398        window: &WindowHandle<T>,
399        read: impl FnOnce(Entity<T>, &App) -> R,
400    ) -> Result<R>
401    where
402        T: 'static,
403    {
404        let app = self.app.borrow();
405        app.read_window(window, read)
406    }
407
408    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
409    where
410        R: Send + 'static,
411    {
412        self.background_executor.spawn(future)
413    }
414
415    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
416    where
417        G: Global,
418    {
419        let app = self.app.borrow();
420        app.read_global(callback)
421    }
422
423    fn insert_global_entity<T: 'static>(&mut self, entity: Entity<T>) {
424        let mut app = self.app.borrow_mut();
425        app.insert_global_entity(entity)
426    }
427
428    fn remove_global_entity<T: 'static>(&mut self, entity: &Entity<T>) {
429        let mut app = self.app.borrow_mut();
430        app.remove_global_entity(entity)
431    }
432
433    fn global_entities<T: 'static>(&self) -> impl Iterator<Item = Entity<T>> {
434        let app = self.app.borrow();
435        let iter = app.global_entities();
436        // Cloning the iterator here so that lock can be released when function is going out of scope
437        iter.collect::<Vec<_>>().into_iter()
438    }
439}