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}