@@ -28,8 +28,8 @@ use crate::{
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
- Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds, WindowHandle,
- WindowOptions,
+ SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
+ WindowHandle, WindowOptions,
app::GpuiMode,
};
use rand::{SeedableRng, rngs::StdRng};
@@ -312,6 +312,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
}
/// Update the root view.
+ /// Automatically draws the window after the update to ensure the scene is current.
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
let result = {
let mut app = self.app.borrow_mut();
@@ -325,6 +326,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
.expect("window not found")
};
self.background_executor.run_until_parked();
+ self.draw();
result
}
@@ -340,7 +342,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
.and_then(|w| w.root.clone())
.and_then(|r| r.downcast::<V>().ok())
.expect("window or root view not found");
- f(view.read(&*app), &*app)
+ f(view.read(&app), &app)
}
/// Get the window title.
@@ -354,6 +356,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
}
/// Simulate a keystroke.
+ /// Automatically draws the window after the keystroke.
pub fn simulate_keystroke(&mut self, keystroke: &str) {
let keystroke = Keystroke::parse(keystroke).unwrap();
{
@@ -365,6 +368,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
.unwrap();
}
self.background_executor.run_until_parked();
+ self.draw();
}
/// Simulate multiple keystrokes (space-separated).
@@ -428,6 +432,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
}
/// Simulate an input event.
+ /// Automatically draws the window after the event.
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
let platform_input = event.to_platform_input();
{
@@ -439,9 +444,11 @@ impl<V: 'static + Render> TestAppWindow<V> {
.unwrap();
}
self.background_executor.run_until_parked();
+ self.draw();
}
/// Simulate resizing the window.
+ /// Automatically draws the window after the resize.
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
let window_id = self.handle.window_id();
let mut app = self.app.borrow_mut();
@@ -452,6 +459,7 @@ impl<V: 'static + Render> TestAppWindow<V> {
}
drop(app);
self.background_executor.run_until_parked();
+ self.draw();
}
/// Force a redraw of the window.
@@ -463,6 +471,18 @@ impl<V: 'static + Render> TestAppWindow<V> {
})
.unwrap();
}
+
+ /// Get a snapshot of the rendered scene for inspection.
+ /// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
+ pub fn scene_snapshot(&self) -> SceneSnapshot {
+ let app = self.app.borrow();
+ let window = app
+ .windows
+ .get(self.handle.window_id())
+ .and_then(|w| w.as_ref())
+ .expect("window not found");
+ window.rendered_frame.scene.snapshot()
+ }
}
impl<V> Clone for TestAppWindow<V> {
@@ -20,6 +20,110 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub(crate) type DrawOrder = u32;
+/// Test-only scene snapshot for inspecting rendered content.
+#[cfg(any(test, feature = "test-support"))]
+pub mod test_scene {
+ use crate::{Bounds, Hsla, Point, ScaledPixels};
+
+ /// A rendered quad (background, border, cursor, selection, etc.)
+ #[derive(Debug, Clone)]
+ pub struct RenderedQuad {
+ /// Bounds in scaled pixels.
+ pub bounds: Bounds<ScaledPixels>,
+ /// Background color (if solid).
+ pub background_color: Option<Hsla>,
+ /// Border color.
+ pub border_color: Hsla,
+ }
+
+ /// A rendered text glyph.
+ #[derive(Debug, Clone)]
+ pub struct RenderedGlyph {
+ /// Origin position in scaled pixels.
+ pub origin: Point<ScaledPixels>,
+ /// Size in scaled pixels.
+ pub size: crate::Size<ScaledPixels>,
+ /// Color of the glyph.
+ pub color: Hsla,
+ }
+
+ /// Snapshot of scene contents for testing.
+ #[derive(Debug, Default)]
+ pub struct SceneSnapshot {
+ /// All rendered quads.
+ pub quads: Vec<RenderedQuad>,
+ /// All rendered text glyphs.
+ pub glyphs: Vec<RenderedGlyph>,
+ /// Number of shadow primitives.
+ pub shadow_count: usize,
+ /// Number of path primitives.
+ pub path_count: usize,
+ /// Number of underline primitives.
+ pub underline_count: usize,
+ /// Number of polychrome sprites (images, emoji).
+ pub polychrome_sprite_count: usize,
+ /// Number of surface primitives.
+ pub surface_count: usize,
+ }
+
+ impl SceneSnapshot {
+ /// Get unique Y positions of quads, sorted.
+ pub fn quad_y_positions(&self) -> Vec<f32> {
+ let mut positions: Vec<f32> = self.quads.iter().map(|q| q.bounds.origin.y.0).collect();
+ positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+ positions.dedup();
+ positions
+ }
+
+ /// Get unique Y positions of glyphs, sorted.
+ pub fn glyph_y_positions(&self) -> Vec<f32> {
+ let mut positions: Vec<f32> = self.glyphs.iter().map(|g| g.origin.y.0).collect();
+ positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+ positions.dedup();
+ positions
+ }
+
+ /// Find quads within a Y range.
+ pub fn quads_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedQuad> {
+ self.quads
+ .iter()
+ .filter(|q| {
+ let y = q.bounds.origin.y.0;
+ y >= min_y && y < max_y
+ })
+ .collect()
+ }
+
+ /// Find glyphs within a Y range.
+ pub fn glyphs_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedGlyph> {
+ self.glyphs
+ .iter()
+ .filter(|g| {
+ let y = g.origin.y.0;
+ y >= min_y && y < max_y
+ })
+ .collect()
+ }
+
+ /// Debug summary string.
+ pub fn summary(&self) -> String {
+ format!(
+ "quads: {}, glyphs: {}, shadows: {}, paths: {}, underlines: {}, polychrome: {}, surfaces: {}",
+ self.quads.len(),
+ self.glyphs.len(),
+ self.shadow_count,
+ self.path_count,
+ self.underline_count,
+ self.polychrome_sprite_count,
+ self.surface_count,
+ )
+ }
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub use test_scene::*;
+
#[derive(Default)]
pub(crate) struct Scene {
pub(crate) paint_operations: Vec<PaintOperation>,
@@ -124,6 +228,36 @@ impl Scene {
}
}
+ /// Create a snapshot of the scene for testing.
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn snapshot(&self) -> SceneSnapshot {
+ let quads = self.quads.iter().map(|q| {
+ RenderedQuad {
+ bounds: q.bounds,
+ background_color: q.background.as_solid(),
+ border_color: q.border_color,
+ }
+ }).collect();
+
+ let glyphs = self.monochrome_sprites.iter().map(|s| {
+ RenderedGlyph {
+ origin: s.bounds.origin,
+ size: s.bounds.size,
+ color: s.color,
+ }
+ }).collect();
+
+ SceneSnapshot {
+ quads,
+ glyphs,
+ shadow_count: self.shadows.len(),
+ path_count: self.paths.len(),
+ underline_count: self.underlines.len(),
+ polychrome_sprite_count: self.polychrome_sprites.len(),
+ surface_count: self.surfaces.len(),
+ }
+ }
+
pub fn finish(&mut self) {
self.shadows.sort_by_key(|shadow| shadow.order);
self.quads.sort_by_key(|quad| quad.order);
@@ -620,7 +754,7 @@ impl Default for TransformationMatrix {
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub order: DrawOrder,
- pub pad: u32, // align to 8 bytes
+ pub pad: u32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
@@ -638,7 +772,7 @@ impl From<MonochromeSprite> for Primitive {
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
- pub pad: u32, // align to 8 bytes
+ pub pad: u32,
pub grayscale: bool,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,