Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:49:37.214Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

headless_app_context.rs

285 lines · 9.4 KB · rust
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, EntityId, 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 with_window<R>(
250        &mut self,
251        entity_id: EntityId,
252        f: impl FnOnce(&mut Window, &mut App) -> R,
253    ) -> Option<R> {
254        let mut lock = self.app.borrow_mut();
255        lock.with_window(entity_id, f)
256    }
257
258    fn read_window<T, R>(
259        &self,
260        window: &WindowHandle<T>,
261        read: impl FnOnce(Entity<T>, &App) -> R,
262    ) -> Result<R>
263    where
264        T: 'static,
265    {
266        let app = self.app.borrow();
267        app.read_window(window, read)
268    }
269
270    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
271    where
272        R: Send + 'static,
273    {
274        self.background_executor.spawn(future)
275    }
276
277    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
278    where
279        G: Global,
280    {
281        let app = self.app.borrow();
282        app.read_global(callback)
283    }
284}
285
Served at tenant.openagents/omega Member data and write actions are omitted.