Add `Harness::new_eframe` and `TestRenderer` trait (#5539)
Co-authored-by: Andreas Reich <r_andreas2@web.de>
This commit is contained in:
parent
ee4ab08c8a
commit
46b58e5bcc
|
|
@ -1319,6 +1319,7 @@ dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_demo_lib",
|
"egui_demo_lib",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
|
"egui_kittest",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"image",
|
"image",
|
||||||
|
|
@ -1395,6 +1396,7 @@ version = "0.30.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dify",
|
"dify",
|
||||||
"document-features",
|
"document-features",
|
||||||
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
"image",
|
"image",
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,28 @@ impl HasDisplayHandle for CreationContext<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CreationContext<'_> {
|
||||||
|
/// Create a new empty [CreationContext] for testing [App]s in kittest.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn _new_kittest(egui_ctx: egui::Context) -> Self {
|
||||||
|
Self {
|
||||||
|
egui_ctx,
|
||||||
|
integration_info: IntegrationInfo::mock(),
|
||||||
|
storage: None,
|
||||||
|
#[cfg(feature = "glow")]
|
||||||
|
gl: None,
|
||||||
|
#[cfg(feature = "glow")]
|
||||||
|
get_proc_address: None,
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
wgpu_render_state: None,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
raw_window_handle: Err(HandleError::NotSupported),
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
raw_display_handle: Err(HandleError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
||||||
|
|
@ -617,7 +639,8 @@ pub struct Frame {
|
||||||
|
|
||||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub(crate) wgpu_render_state: Option<egui_wgpu::RenderState>,
|
#[doc(hidden)]
|
||||||
|
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||||
|
|
||||||
/// Raw platform window handle
|
/// Raw platform window handle
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -651,6 +674,25 @@ impl HasDisplayHandle for Frame {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
|
/// Create a new empty [Frame] for testing [App]s in kittest.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn _new_kittest() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "glow")]
|
||||||
|
gl: None,
|
||||||
|
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||||
|
glow_register_native_texture: None,
|
||||||
|
info: IntegrationInfo::mock(),
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
raw_display_handle: Err(HandleError::NotSupported),
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
raw_window_handle: Err(HandleError::NotSupported),
|
||||||
|
storage: None,
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
wgpu_render_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// True if you are in a web environment.
|
/// True if you are in a web environment.
|
||||||
///
|
///
|
||||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||||
|
|
@ -794,6 +836,29 @@ pub struct IntegrationInfo {
|
||||||
pub cpu_usage: Option<f32>,
|
pub cpu_usage: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntegrationInfo {
|
||||||
|
fn mock() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
web_info: WebInfo {
|
||||||
|
user_agent: "kittest".to_owned(),
|
||||||
|
location: Location {
|
||||||
|
url: "http://localhost".to_owned(),
|
||||||
|
protocol: "http:".to_owned(),
|
||||||
|
host: "localhost".to_owned(),
|
||||||
|
hostname: "localhost".to_owned(),
|
||||||
|
port: "80".to_owned(),
|
||||||
|
hash: String::new(),
|
||||||
|
query: String::new(),
|
||||||
|
query_map: Default::default(),
|
||||||
|
origin: "http://localhost".to_owned(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cpu_usage: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ impl WebPainterWgpu {
|
||||||
let render_state = RenderState::create(
|
let render_state = RenderState::create(
|
||||||
&options.wgpu_options,
|
&options.wgpu_options,
|
||||||
&instance,
|
&instance,
|
||||||
&surface,
|
Some(&surface),
|
||||||
depth_format,
|
depth_format,
|
||||||
1,
|
1,
|
||||||
options.dithering,
|
options.dithering,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub use wgpu;
|
||||||
mod renderer;
|
mod renderer;
|
||||||
|
|
||||||
pub use renderer::*;
|
pub use renderer::*;
|
||||||
use wgpu::{Adapter, Device, Instance, Queue};
|
use wgpu::{Adapter, Device, Instance, Queue, TextureFormat};
|
||||||
|
|
||||||
/// Helpers for capturing screenshots of the UI.
|
/// Helpers for capturing screenshots of the UI.
|
||||||
pub mod capture;
|
pub mod capture;
|
||||||
|
|
@ -91,7 +91,7 @@ impl RenderState {
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
config: &WgpuConfiguration,
|
config: &WgpuConfiguration,
|
||||||
instance: &wgpu::Instance,
|
instance: &wgpu::Instance,
|
||||||
surface: &wgpu::Surface<'static>,
|
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
depth_format: Option<wgpu::TextureFormat>,
|
||||||
msaa_samples: u32,
|
msaa_samples: u32,
|
||||||
dithering: bool,
|
dithering: bool,
|
||||||
|
|
@ -113,7 +113,7 @@ impl RenderState {
|
||||||
instance
|
instance
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
power_preference,
|
power_preference,
|
||||||
compatible_surface: Some(surface),
|
compatible_surface,
|
||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
@ -186,11 +186,14 @@ impl RenderState {
|
||||||
} => (adapter, device, queue),
|
} => (adapter, device, queue),
|
||||||
};
|
};
|
||||||
|
|
||||||
let capabilities = {
|
let surface_formats = {
|
||||||
profiling::scope!("get_capabilities");
|
profiling::scope!("get_capabilities");
|
||||||
surface.get_capabilities(&adapter).formats
|
compatible_surface.map_or_else(
|
||||||
|
|| vec![TextureFormat::Rgba8Unorm],
|
||||||
|
|s| s.get_capabilities(&adapter).formats,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let target_format = crate::preferred_framebuffer_format(&capabilities)?;
|
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
||||||
|
|
||||||
let renderer = Renderer::new(
|
let renderer = Renderer::new(
|
||||||
&device,
|
&device,
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ impl Painter {
|
||||||
let render_state = RenderState::create(
|
let render_state = RenderState::create(
|
||||||
&self.configuration,
|
&self.configuration,
|
||||||
&self.instance,
|
&self.instance,
|
||||||
&surface,
|
Some(&surface),
|
||||||
self.depth_format,
|
self.depth_format,
|
||||||
self.msaa_samples,
|
self.msaa_samples,
|
||||||
self.dithering,
|
self.dithering,
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,6 @@ rfd = { version = "0.15", optional = true }
|
||||||
wasm-bindgen = "=0.2.95"
|
wasm-bindgen = "=0.2.95"
|
||||||
wasm-bindgen-futures.workspace = true
|
wasm-bindgen-futures.workspace = true
|
||||||
web-sys.workspace = true
|
web-sys.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] }
|
||||||
|
|
@ -54,8 +54,9 @@ impl eframe::App for ImageViewer {
|
||||||
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||||
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
ui.label("URI:");
|
let label = ui.label("URI:");
|
||||||
ui.text_edit_singleline(&mut self.uri_edit_text);
|
ui.text_edit_singleline(&mut self.uri_edit_text)
|
||||||
|
.labelled_by(label.id);
|
||||||
if ui.small_button("✔").clicked() {
|
if ui.small_button("✔").clicked() {
|
||||||
ctx.forget_image(&self.current_uri);
|
ctx.forget_image(&self.current_uri);
|
||||||
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ mod backend_panel;
|
||||||
mod frame_history;
|
mod frame_history;
|
||||||
mod wrap_app;
|
mod wrap_app;
|
||||||
|
|
||||||
pub use wrap_app::WrapApp;
|
pub use wrap_app::{Anchor, WrapApp};
|
||||||
|
|
||||||
/// Time of day as seconds since midnight. Used for clock in demo app.
|
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||||
pub(crate) fn seconds_since_midnight() -> f64 {
|
pub(crate) fn seconds_since_midnight() -> f64 {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ impl eframe::App for DemoApp {
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct FractalClockApp {
|
pub struct FractalClockApp {
|
||||||
fractal_clock: crate::apps::FractalClock,
|
fractal_clock: crate::apps::FractalClock,
|
||||||
|
pub mock_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for FractalClockApp {
|
impl eframe::App for FractalClockApp {
|
||||||
|
|
@ -46,7 +47,7 @@ impl eframe::App for FractalClockApp {
|
||||||
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
self.fractal_clock
|
self.fractal_clock
|
||||||
.ui(ui, Some(crate::seconds_since_midnight()));
|
.ui(ui, self.mock_time.or(Some(crate::seconds_since_midnight())));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +78,7 @@ impl eframe::App for ColorTestApp {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
enum Anchor {
|
pub enum Anchor {
|
||||||
Demo,
|
Demo,
|
||||||
|
|
||||||
EasyMarkEditor,
|
EasyMarkEditor,
|
||||||
|
|
@ -161,7 +162,7 @@ pub struct State {
|
||||||
http: crate::apps::HttpApp,
|
http: crate::apps::HttpApp,
|
||||||
#[cfg(feature = "image_viewer")]
|
#[cfg(feature = "image_viewer")]
|
||||||
image_viewer: crate::apps::ImageViewer,
|
image_viewer: crate::apps::ImageViewer,
|
||||||
clock: FractalClockApp,
|
pub clock: FractalClockApp,
|
||||||
rendering_test: ColorTestApp,
|
rendering_test: ColorTestApp,
|
||||||
|
|
||||||
selected_anchor: Anchor,
|
selected_anchor: Anchor,
|
||||||
|
|
@ -170,7 +171,7 @@ pub struct State {
|
||||||
|
|
||||||
/// Wraps many demo/test apps into one.
|
/// Wraps many demo/test apps into one.
|
||||||
pub struct WrapApp {
|
pub struct WrapApp {
|
||||||
state: State,
|
pub state: State,
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
custom3d: Option<crate::apps::Custom3d>,
|
custom3d: Option<crate::apps::Custom3d>,
|
||||||
|
|
@ -203,7 +204,9 @@ impl WrapApp {
|
||||||
slf
|
slf
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, Anchor, &mut dyn eframe::App)> {
|
pub fn apps_iter_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> impl Iterator<Item = (&'static str, Anchor, &mut dyn eframe::App)> {
|
||||||
let mut vec = vec![
|
let mut vec = vec![
|
||||||
(
|
(
|
||||||
"✨ Demos",
|
"✨ Demos",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7c05cc3d48242e46a391af34cb56f72de7933bf2cead009b6cd477c21867a84e
|
||||||
|
size 327802
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:61212e30fe1fecf5891ddad6ac795df510bfad76b21a7a8a13aa024fdad6d05e
|
||||||
|
size 93118
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7bcf6e2977bed682d7bdaa0b6a6786e528662dd0791d2e6f83cf1b4852035838
|
||||||
|
size 182833
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e6cc6ff64eb73ddac89ecdacd07c2176f3ab952c0db4593fccf6d11f155ec392
|
||||||
|
size 103100
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
use egui::accesskit::Role;
|
||||||
|
use egui::Vec2;
|
||||||
|
use egui_demo_app::{Anchor, WrapApp};
|
||||||
|
use egui_kittest::kittest::Queryable;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_demo_app() {
|
||||||
|
let mut harness = egui_kittest::Harness::builder()
|
||||||
|
.with_size(Vec2::new(900.0, 600.0))
|
||||||
|
.wgpu()
|
||||||
|
.build_eframe(|cc| WrapApp::new(cc));
|
||||||
|
|
||||||
|
let app = harness.state_mut();
|
||||||
|
|
||||||
|
// Mock the fractal clock time so snapshots are consistent.
|
||||||
|
app.state.clock.mock_time = Some(36383.0);
|
||||||
|
|
||||||
|
let apps = app
|
||||||
|
.apps_iter_mut()
|
||||||
|
.map(|(name, anchor, _)| (name, anchor))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
assert!(
|
||||||
|
apps.iter()
|
||||||
|
.any(|(_, anchor)| matches!(anchor, Anchor::Custom3d)),
|
||||||
|
"Expected to find the Custom3d app.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut results = vec![];
|
||||||
|
|
||||||
|
for (name, anchor) in apps {
|
||||||
|
harness.get_by_role_and_label(Role::Button, name).click();
|
||||||
|
|
||||||
|
match anchor {
|
||||||
|
// The widget gallery demo shows the current date, so we can't use it for snapshot testing
|
||||||
|
Anchor::Demo => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// This is already tested extensively elsewhere
|
||||||
|
Anchor::Rendering => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We don't want to rely on a network connection for tests
|
||||||
|
#[cfg(feature = "http")]
|
||||||
|
Anchor::Http => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Load a local image where we know it exists and loads quickly
|
||||||
|
#[cfg(feature = "image_viewer")]
|
||||||
|
Anchor::ImageViewer => {
|
||||||
|
harness.run();
|
||||||
|
|
||||||
|
harness
|
||||||
|
.get_by_role_and_label(Role::TextInput, "URI:")
|
||||||
|
.focus();
|
||||||
|
harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||||
|
|
||||||
|
harness
|
||||||
|
.get_by_role_and_label(Role::TextInput, "URI:")
|
||||||
|
.type_text("file://../eframe/data/icon.png");
|
||||||
|
|
||||||
|
harness.get_by_role_and_label(Role::Button, "✔").click();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
harness.run();
|
||||||
|
|
||||||
|
if let Err(e) = harness.try_snapshot(&anchor.to_string()) {
|
||||||
|
results.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = results.first() {
|
||||||
|
panic!("{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -405,7 +405,7 @@ mod tests {
|
||||||
options.threshold = 2.1;
|
options.threshold = 2.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options);
|
let result = harness.try_snapshot_options(&format!("demos/{name}"), &options);
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
errors.push(err.to_string());
|
errors.push(err.to_string());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,21 +235,21 @@ mod tests {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
harness.run();
|
harness.run();
|
||||||
results.push(harness.try_wgpu_snapshot("modals_1"));
|
results.push(harness.try_snapshot("modals_1"));
|
||||||
|
|
||||||
harness.get_by_label("Save").click();
|
harness.get_by_label("Save").click();
|
||||||
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
harness.run();
|
||||||
results.push(harness.try_wgpu_snapshot("modals_2"));
|
results.push(harness.try_snapshot("modals_2"));
|
||||||
|
|
||||||
harness.get_by_label("Yes Please").click();
|
harness.get_by_label("Yes Please").click();
|
||||||
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
harness.run();
|
||||||
results.push(harness.try_wgpu_snapshot("modals_3"));
|
results.push(harness.try_snapshot("modals_3"));
|
||||||
|
|
||||||
for result in results {
|
for result in results {
|
||||||
result.unwrap();
|
result.unwrap();
|
||||||
|
|
@ -282,6 +282,6 @@ mod tests {
|
||||||
harness.run();
|
harness.run();
|
||||||
|
|
||||||
// This snapshots should show the progress bar modal on top of the save modal.
|
// This snapshots should show the progress bar modal on top of the save modal.
|
||||||
harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area");
|
harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,6 @@ mod tests {
|
||||||
|
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
|
|
||||||
harness.wgpu_snapshot("widget_gallery");
|
harness.snapshot("widget_gallery");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -703,7 +703,7 @@ mod tests {
|
||||||
|
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
|
|
||||||
let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
|
let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
errors.push(err);
|
errors.push(err);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,19 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Adds a wgpu-based test renderer.
|
# Adds a wgpu-based test renderer.
|
||||||
wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image"]
|
wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"]
|
||||||
|
|
||||||
# Adds a dify-based image snapshot utility.
|
# Adds a dify-based image snapshot utility.
|
||||||
snapshot = ["dep:dify", "dep:image", "image/png"]
|
snapshot = ["dep:dify", "dep:image", "image/png"]
|
||||||
|
|
||||||
|
# Allows testing eframe::App
|
||||||
|
eframe = ["dep:eframe", "eframe/accesskit"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kittest.workspace = true
|
kittest.workspace = true
|
||||||
egui = { workspace = true, features = ["accesskit"] }
|
egui = { workspace = true, features = ["accesskit"] }
|
||||||
|
eframe = { workspace = true, optional = true }
|
||||||
|
|
||||||
# wgpu dependencies
|
# wgpu dependencies
|
||||||
egui-wgpu = { workspace = true, optional = true }
|
egui-wgpu = { workspace = true, optional = true }
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,13 @@ fn main() {
|
||||||
|
|
||||||
// You can even render the ui and do image snapshot tests
|
// You can even render the ui and do image snapshot tests
|
||||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||||
harness.wgpu_snapshot("readme_example");
|
harness.snapshot("readme_example");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Snapshot testing
|
## Snapshot testing
|
||||||
There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features.
|
There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features.
|
||||||
Once enabled, you can call `Harness::wgpu_snapshot` to render the ui and save the image to the `tests/snapshots` directory.
|
Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory.
|
||||||
|
|
||||||
To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`.
|
To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`.
|
||||||
Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass.
|
Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass.
|
||||||
|
|
|
||||||
|
|
@ -5,37 +5,22 @@ type AppKindUiState<'a, State> = Box<dyn FnMut(&mut egui::Ui, &mut State) + 'a>;
|
||||||
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
|
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
|
||||||
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;
|
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;
|
||||||
|
|
||||||
|
/// In order to access the [`eframe::App`] trait from the generic `State`, we store a function pointer
|
||||||
|
/// here that will return the dyn trait from the struct. In the builder we have the correct where
|
||||||
|
/// clause to be able to create this.
|
||||||
|
/// Later we can use it anywhere to get the [`eframe::App`] from the `State`.
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
type AppKindEframe<'a, State> = (fn(&mut State) -> &mut dyn eframe::App, eframe::Frame);
|
||||||
|
|
||||||
pub(crate) enum AppKind<'a, State> {
|
pub(crate) enum AppKind<'a, State> {
|
||||||
Context(AppKindContext<'a>),
|
Context(AppKindContext<'a>),
|
||||||
Ui(AppKindUi<'a>),
|
Ui(AppKindUi<'a>),
|
||||||
ContextState(AppKindContextState<'a, State>),
|
ContextState(AppKindContextState<'a, State>),
|
||||||
UiState(AppKindUiState<'a, State>),
|
UiState(AppKindUiState<'a, State>),
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
Eframe(AppKindEframe<'a, State>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(lucasmerlin): These aren't working unfortunately :(
|
|
||||||
// I think they should work though: https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions/
|
|
||||||
// pub trait IntoAppKind<'a, UiKind> {
|
|
||||||
// fn into_harness_kind(self) -> AppKind<'a>;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// impl<'a, F> IntoAppKind<'a, &egui::Context> for F
|
|
||||||
// where
|
|
||||||
// F: FnMut(&egui::Context) + 'a,
|
|
||||||
// {
|
|
||||||
// fn into_harness_kind(self) -> AppKind<'a> {
|
|
||||||
// AppKind::Context(Box::new(self))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// impl<'a, F> IntoAppKind<'a, &mut egui::Ui> for F
|
|
||||||
// where
|
|
||||||
// F: FnMut(&mut egui::Ui) + 'a,
|
|
||||||
// {
|
|
||||||
// fn into_harness_kind(self) -> AppKind<'a> {
|
|
||||||
// AppKind::Ui(Box::new(self))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl<'a, State> AppKind<'a, State> {
|
impl<'a, State> AppKind<'a, State> {
|
||||||
pub fn run(
|
pub fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -54,6 +39,12 @@ impl<'a, State> AppKind<'a, State> {
|
||||||
f(ctx, state);
|
f(ctx, state);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
AppKind::Eframe((get_app, frame)) => {
|
||||||
|
let app = get_app(state);
|
||||||
|
app.update(ctx, frame);
|
||||||
|
None
|
||||||
|
}
|
||||||
kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)),
|
kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +69,9 @@ impl<'a, State> AppKind<'a, State> {
|
||||||
.show(ui, |ui| match self {
|
.show(ui, |ui| match self {
|
||||||
AppKind::Ui(f) => f(ui),
|
AppKind::Ui(f) => f(ui),
|
||||||
AppKind::UiState(f) => f(ui, state),
|
AppKind::UiState(f) => f(ui, state),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(
|
||||||
|
"run_ui should only be called with AppKind::Ui or AppKind UiState"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::app_kind::AppKind;
|
use crate::app_kind::AppKind;
|
||||||
use crate::Harness;
|
use crate::wgpu::WgpuTestRenderer;
|
||||||
|
use crate::{Harness, LazyRenderer, TestRenderer};
|
||||||
use egui::{Pos2, Rect, Vec2};
|
use egui::{Pos2, Rect, Vec2};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
|
@ -8,6 +9,7 @@ pub struct HarnessBuilder<State = ()> {
|
||||||
pub(crate) screen_rect: Rect,
|
pub(crate) screen_rect: Rect,
|
||||||
pub(crate) pixels_per_point: f32,
|
pub(crate) pixels_per_point: f32,
|
||||||
pub(crate) state: PhantomData<State>,
|
pub(crate) state: PhantomData<State>,
|
||||||
|
pub(crate) renderer: Box<dyn TestRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State> Default for HarnessBuilder<State> {
|
impl<State> Default for HarnessBuilder<State> {
|
||||||
|
|
@ -16,6 +18,7 @@ impl<State> Default for HarnessBuilder<State> {
|
||||||
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
|
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
|
||||||
pixels_per_point: 1.0,
|
pixels_per_point: 1.0,
|
||||||
state: PhantomData,
|
state: PhantomData,
|
||||||
|
renderer: Box::new(LazyRenderer::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +40,29 @@ impl<State> HarnessBuilder<State> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the [`TestRenderer`] to use for rendering.
|
||||||
|
///
|
||||||
|
/// By default, a [`LazyRenderer`] is used.
|
||||||
|
#[inline]
|
||||||
|
pub fn renderer(mut self, renderer: impl TestRenderer + 'static) -> Self {
|
||||||
|
self.renderer = Box::new(renderer);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable wgpu rendering with a default setup suitable for testing.
|
||||||
|
///
|
||||||
|
/// This sets up a [`WgpuTestRenderer`] with the default setup.
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
pub fn wgpu(self) -> Self {
|
||||||
|
self.renderer(WgpuTestRenderer::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable wgpu rendering with the given setup.
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
pub fn wgpu_setup(self, setup: egui_wgpu::WgpuSetup) -> Self {
|
||||||
|
self.renderer(WgpuTestRenderer::from_setup(setup))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new Harness with the given app closure and a state.
|
/// Create a new Harness with the given app closure and a state.
|
||||||
///
|
///
|
||||||
/// The app closure will immediately be called once to create the initial ui.
|
/// The app closure will immediately be called once to create the initial ui.
|
||||||
|
|
@ -66,7 +92,7 @@ impl<State> HarnessBuilder<State> {
|
||||||
app: impl FnMut(&egui::Context, &mut State) + 'a,
|
app: impl FnMut(&egui::Context, &mut State) + 'a,
|
||||||
state: State,
|
state: State,
|
||||||
) -> Harness<'a, State> {
|
) -> Harness<'a, State> {
|
||||||
Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state)
|
Harness::from_builder(self, AppKind::ContextState(Box::new(app)), state, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new Harness with the given ui closure and a state.
|
/// Create a new Harness with the given ui closure and a state.
|
||||||
|
|
@ -95,7 +121,30 @@ impl<State> HarnessBuilder<State> {
|
||||||
app: impl FnMut(&mut egui::Ui, &mut State) + 'a,
|
app: impl FnMut(&mut egui::Ui, &mut State) + 'a,
|
||||||
state: State,
|
state: State,
|
||||||
) -> Harness<'a, State> {
|
) -> Harness<'a, State> {
|
||||||
Harness::from_builder(&self, AppKind::UiState(Box::new(app)), state)
|
Harness::from_builder(self, AppKind::UiState(Box::new(app)), state, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [Harness] from the given eframe creation closure.
|
||||||
|
/// The app can be accessed via the [`Harness::state`] / [`Harness::state_mut`] methods.
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
pub fn build_eframe<'a>(
|
||||||
|
self,
|
||||||
|
build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State,
|
||||||
|
) -> Harness<'a, State>
|
||||||
|
where
|
||||||
|
State: eframe::App,
|
||||||
|
{
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
|
||||||
|
let mut cc = eframe::CreationContext::_new_kittest(ctx.clone());
|
||||||
|
let mut frame = eframe::Frame::_new_kittest();
|
||||||
|
|
||||||
|
self.renderer.setup_eframe(&mut cc, &mut frame);
|
||||||
|
|
||||||
|
let app = build(&mut cc);
|
||||||
|
|
||||||
|
let kind = AppKind::Eframe((|state| state, frame));
|
||||||
|
Harness::from_builder(self, kind, app, Some(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +168,7 @@ impl HarnessBuilder {
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
|
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
|
||||||
Harness::from_builder(&self, AppKind::Context(Box::new(app)), ())
|
Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new Harness with the given ui closure.
|
/// Create a new Harness with the given ui closure.
|
||||||
|
|
@ -138,6 +187,6 @@ impl HarnessBuilder {
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> {
|
pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> {
|
||||||
Harness::from_builder(&self, AppKind::Ui(Box::new(app)), ())
|
Harness::from_builder(self, AppKind::Ui(Box::new(app)), (), None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,21 @@ mod snapshot;
|
||||||
pub use snapshot::*;
|
pub use snapshot::*;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
mod app_kind;
|
mod app_kind;
|
||||||
|
mod renderer;
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
mod texture_to_image;
|
mod texture_to_image;
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub mod wgpu;
|
pub mod wgpu;
|
||||||
|
|
||||||
pub use kittest;
|
pub use kittest;
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
use crate::app_kind::AppKind;
|
use crate::app_kind::AppKind;
|
||||||
use crate::event::EventState;
|
use crate::event::EventState;
|
||||||
|
|
||||||
pub use builder::*;
|
pub use builder::*;
|
||||||
use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId};
|
pub use renderer::*;
|
||||||
|
|
||||||
|
use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId};
|
||||||
use kittest::{Node, Queryable};
|
use kittest::{Node, Queryable};
|
||||||
|
|
||||||
/// The test Harness. This contains everything needed to run the test.
|
/// The test Harness. This contains everything needed to run the test.
|
||||||
|
|
@ -37,11 +40,11 @@ pub struct Harness<'a, State = ()> {
|
||||||
input: egui::RawInput,
|
input: egui::RawInput,
|
||||||
kittest: kittest::State,
|
kittest: kittest::State,
|
||||||
output: egui::FullOutput,
|
output: egui::FullOutput,
|
||||||
texture_deltas: Vec<TexturesDelta>,
|
|
||||||
app: AppKind<'a, State>,
|
app: AppKind<'a, State>,
|
||||||
event_state: EventState,
|
event_state: EventState,
|
||||||
response: Option<egui::Response>,
|
response: Option<egui::Response>,
|
||||||
state: State,
|
state: State,
|
||||||
|
renderer: Box<dyn TestRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, State> Debug for Harness<'a, State> {
|
impl<'a, State> Debug for Harness<'a, State> {
|
||||||
|
|
@ -52,11 +55,12 @@ impl<'a, State> Debug for Harness<'a, State> {
|
||||||
|
|
||||||
impl<'a, State> Harness<'a, State> {
|
impl<'a, State> Harness<'a, State> {
|
||||||
pub(crate) fn from_builder(
|
pub(crate) fn from_builder(
|
||||||
builder: &HarnessBuilder<State>,
|
builder: HarnessBuilder<State>,
|
||||||
mut app: AppKind<'a, State>,
|
mut app: AppKind<'a, State>,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
|
ctx: Option<egui::Context>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ctx = egui::Context::default();
|
let ctx = ctx.unwrap_or_default();
|
||||||
ctx.enable_accesskit();
|
ctx.enable_accesskit();
|
||||||
let mut input = egui::RawInput {
|
let mut input = egui::RawInput {
|
||||||
screen_rect: Some(builder.screen_rect),
|
screen_rect: Some(builder.screen_rect),
|
||||||
|
|
@ -73,6 +77,9 @@ impl<'a, State> Harness<'a, State> {
|
||||||
response = app.run(ctx, &mut state, false);
|
response = app.run(ctx, &mut state, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut renderer = builder.renderer;
|
||||||
|
renderer.handle_delta(&output.textures_delta);
|
||||||
|
|
||||||
let mut harness = Self {
|
let mut harness = Self {
|
||||||
app,
|
app,
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -84,11 +91,11 @@ impl<'a, State> Harness<'a, State> {
|
||||||
.take()
|
.take()
|
||||||
.expect("AccessKit was disabled"),
|
.expect("AccessKit was disabled"),
|
||||||
),
|
),
|
||||||
texture_deltas: vec![mem::take(&mut output.textures_delta)],
|
|
||||||
output,
|
output,
|
||||||
response,
|
response,
|
||||||
event_state: EventState::default(),
|
event_state: EventState::default(),
|
||||||
state,
|
state,
|
||||||
|
renderer,
|
||||||
};
|
};
|
||||||
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
||||||
harness.run();
|
harness.run();
|
||||||
|
|
@ -153,6 +160,15 @@ impl<'a, State> Harness<'a, State> {
|
||||||
Self::builder().build_ui_state(app, state)
|
Self::builder().build_ui_state(app, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new [Harness] from the given eframe creation closure.
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
|
||||||
|
where
|
||||||
|
State: eframe::App,
|
||||||
|
{
|
||||||
|
Self::builder().build_eframe(builder)
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the size of the window.
|
/// Set the size of the window.
|
||||||
/// Note: If you only want to set the size once at the beginning,
|
/// Note: If you only want to set the size once at the beginning,
|
||||||
/// prefer using [`HarnessBuilder::with_size`].
|
/// prefer using [`HarnessBuilder::with_size`].
|
||||||
|
|
@ -194,8 +210,7 @@ impl<'a, State> Harness<'a, State> {
|
||||||
.take()
|
.take()
|
||||||
.expect("AccessKit was disabled"),
|
.expect("AccessKit was disabled"),
|
||||||
);
|
);
|
||||||
self.texture_deltas
|
self.renderer.handle_delta(&output.textures_delta);
|
||||||
.push(mem::take(&mut output.textures_delta));
|
|
||||||
self.output = output;
|
self.output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,21 +268,35 @@ impl<'a, State> Harness<'a, State> {
|
||||||
/// Press a key.
|
/// Press a key.
|
||||||
/// This will create a key down event and a key up event.
|
/// This will create a key down event and a key up event.
|
||||||
pub fn press_key(&mut self, key: egui::Key) {
|
pub fn press_key(&mut self, key: egui::Key) {
|
||||||
|
self.press_key_modifiers(Modifiers::default(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press a key with modifiers.
|
||||||
|
/// This will create a key down event and a key up event.
|
||||||
|
pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) {
|
||||||
self.input.events.push(egui::Event::Key {
|
self.input.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers: Default::default(),
|
modifiers,
|
||||||
repeat: false,
|
repeat: false,
|
||||||
physical_key: None,
|
physical_key: None,
|
||||||
});
|
});
|
||||||
self.input.events.push(egui::Event::Key {
|
self.input.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers: Default::default(),
|
modifiers,
|
||||||
repeat: false,
|
repeat: false,
|
||||||
physical_key: None,
|
physical_key: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the last output to an image.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the rendering fails.
|
||||||
|
pub fn render(&mut self) -> Result<image::RgbaImage, String> {
|
||||||
|
self.renderer.render(&self.ctx, &self.output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utilities for stateless harnesses.
|
/// Utilities for stateless harnesses.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
use egui::{Context, FullOutput, TexturesDelta};
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
|
pub trait TestRenderer {
|
||||||
|
/// We use this to pass the glow / wgpu render state to [`eframe::Frame`].
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {}
|
||||||
|
|
||||||
|
/// Handle a [`TexturesDelta`] by updating the renderer's textures.
|
||||||
|
fn handle_delta(&mut self, delta: &TexturesDelta);
|
||||||
|
|
||||||
|
/// Render the [`crate::Harness`] and return the resulting image.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the rendering fails.
|
||||||
|
fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result<RgbaImage, String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A lazy renderer that initializes the renderer on the first render call.
|
||||||
|
///
|
||||||
|
/// By default, this will create a wgpu renderer if the wgpu feature is enabled.
|
||||||
|
pub enum LazyRenderer {
|
||||||
|
Uninitialized {
|
||||||
|
texture_ops: Vec<egui::TexturesDelta>,
|
||||||
|
builder: Option<Box<dyn FnOnce() -> Box<dyn TestRenderer>>>,
|
||||||
|
},
|
||||||
|
Initialized {
|
||||||
|
renderer: Box<dyn TestRenderer>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LazyRenderer {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
return Self::new(crate::wgpu::WgpuTestRenderer::new);
|
||||||
|
#[cfg(not(feature = "wgpu"))]
|
||||||
|
return Self::Uninitialized {
|
||||||
|
texture_ops: Vec::new(),
|
||||||
|
builder: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LazyRenderer {
|
||||||
|
pub fn new<T: TestRenderer + 'static>(create_renderer: impl FnOnce() -> T + 'static) -> Self {
|
||||||
|
Self::Uninitialized {
|
||||||
|
texture_ops: Vec::new(),
|
||||||
|
builder: Some(Box::new(move || Box::new(create_renderer()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestRenderer for LazyRenderer {
|
||||||
|
fn handle_delta(&mut self, delta: &TexturesDelta) {
|
||||||
|
match self {
|
||||||
|
Self::Uninitialized { texture_ops, .. } => texture_ops.push(delta.clone()),
|
||||||
|
Self::Initialized { renderer } => renderer.handle_delta(delta),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result<RgbaImage, String> {
|
||||||
|
match self {
|
||||||
|
Self::Uninitialized {
|
||||||
|
texture_ops,
|
||||||
|
builder: build,
|
||||||
|
} => {
|
||||||
|
let mut renderer = build.take().ok_or({
|
||||||
|
"No default renderer available. \
|
||||||
|
Enable the wgpu feature or set one via HarnessBuilder::renderer"
|
||||||
|
})?();
|
||||||
|
for delta in texture_ops.drain(..) {
|
||||||
|
renderer.handle_delta(&delta);
|
||||||
|
}
|
||||||
|
let image = renderer.render(ctx, output)?;
|
||||||
|
*self = Self::Initialized { renderer };
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
Self::Initialized { renderer } => renderer.render(ctx, output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,12 @@ pub enum SnapshotError {
|
||||||
/// The error that occurred
|
/// The error that occurred
|
||||||
err: ImageError,
|
err: ImageError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Error rendering the image
|
||||||
|
RenderError {
|
||||||
|
/// The error that occurred
|
||||||
|
err: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOW_TO_UPDATE_SCREENSHOTS: &str =
|
const HOW_TO_UPDATE_SCREENSHOTS: &str =
|
||||||
|
|
@ -142,6 +148,9 @@ impl Display for SnapshotError {
|
||||||
let path = std::path::absolute(path).unwrap_or(path.clone());
|
let path = std::path::absolute(path).unwrap_or(path.clone());
|
||||||
write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}")
|
write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}")
|
||||||
}
|
}
|
||||||
|
Self::RenderError { err } => {
|
||||||
|
write!(f, "Error rendering image: {err:?}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +324,7 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) {
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
impl<State> Harness<'_, State> {
|
impl<State> Harness<'_, State> {
|
||||||
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
|
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot
|
||||||
/// with custom options.
|
/// with custom options.
|
||||||
///
|
///
|
||||||
/// If you want to change the default options for your whole project, you could create an
|
/// If you want to change the default options for your whole project, you could create an
|
||||||
|
|
@ -323,7 +332,7 @@ impl<State> Harness<'_, State> {
|
||||||
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
||||||
/// You could additionally use the
|
/// You could additionally use the
|
||||||
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
||||||
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
|
/// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults.
|
||||||
///
|
///
|
||||||
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
|
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
|
||||||
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
||||||
|
|
@ -331,31 +340,35 @@ impl<State> Harness<'_, State> {
|
||||||
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
|
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
|
||||||
/// reading or writing the snapshot.
|
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||||
pub fn try_wgpu_snapshot_options(
|
pub fn try_snapshot_options(
|
||||||
&self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
options: &SnapshotOptions,
|
options: &SnapshotOptions,
|
||||||
) -> Result<(), SnapshotError> {
|
) -> Result<(), SnapshotError> {
|
||||||
let image = crate::wgpu::TestRenderer::new().render(self);
|
let image = self
|
||||||
|
.render()
|
||||||
|
.map_err(|err| SnapshotError::RenderError { err })?;
|
||||||
try_image_snapshot_options(&image, name, options)
|
try_image_snapshot_options(&image, name, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
|
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
|
||||||
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
|
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
|
||||||
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
|
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
|
||||||
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
|
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
|
||||||
/// reading or writing the snapshot.
|
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||||
pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> {
|
pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
|
||||||
let image = crate::wgpu::TestRenderer::new().render(self);
|
let image = self
|
||||||
|
.render()
|
||||||
|
.map_err(|err| SnapshotError::RenderError { err })?;
|
||||||
try_image_snapshot(&image, name)
|
try_image_snapshot(&image, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
|
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot
|
||||||
/// with custom options.
|
/// with custom options.
|
||||||
///
|
///
|
||||||
/// If you want to change the default options for your whole project, you could create an
|
/// If you want to change the default options for your whole project, you could create an
|
||||||
|
|
@ -363,7 +376,7 @@ impl<State> Harness<'_, State> {
|
||||||
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
||||||
/// You could additionally use the
|
/// You could additionally use the
|
||||||
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
||||||
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
|
/// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults.
|
||||||
///
|
///
|
||||||
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
|
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
|
||||||
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
||||||
|
|
@ -371,11 +384,11 @@ impl<State> Harness<'_, State> {
|
||||||
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||||
/// snapshot.
|
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) {
|
pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
|
||||||
match self.try_wgpu_snapshot_options(name, options) {
|
match self.try_snapshot_options(name, options) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!("{}", err);
|
panic!("{}", err);
|
||||||
|
|
@ -383,17 +396,17 @@ impl<State> Harness<'_, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
|
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
|
||||||
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
|
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
|
||||||
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
|
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
|
||||||
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||||
/// snapshot.
|
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn wgpu_snapshot(&self, name: &str) {
|
pub fn snapshot(&mut self, name: &str) {
|
||||||
match self.try_wgpu_snapshot(name) {
|
match self.try_snapshot(name) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!("{}", err);
|
panic!("{}", err);
|
||||||
|
|
@ -401,3 +414,45 @@ impl<State> Harness<'_, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated wgpu_snapshot functions
|
||||||
|
// TODO(lucasmerlin): Remove in 0.32
|
||||||
|
#[allow(clippy::missing_errors_doc)]
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
impl<State> Harness<'_, State> {
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.31.0",
|
||||||
|
note = "Use `try_snapshot_options` instead. This function will be removed in 0.32"
|
||||||
|
)]
|
||||||
|
pub fn try_wgpu_snapshot_options(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
options: &SnapshotOptions,
|
||||||
|
) -> Result<(), SnapshotError> {
|
||||||
|
self.try_snapshot_options(name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.31.0",
|
||||||
|
note = "Use `try_snapshot` instead. This function will be removed in 0.32"
|
||||||
|
)]
|
||||||
|
pub fn try_wgpu_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
|
||||||
|
self.try_snapshot(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.31.0",
|
||||||
|
note = "Use `snapshot_options` instead. This function will be removed in 0.32"
|
||||||
|
)]
|
||||||
|
pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
|
||||||
|
self.snapshot_options(name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.31.0",
|
||||||
|
note = "Use `snapshot` instead. This function will be removed in 0.32"
|
||||||
|
)]
|
||||||
|
pub fn wgpu_snapshot(&mut self, name: &str) {
|
||||||
|
self.snapshot(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,122 +1,152 @@
|
||||||
use crate::texture_to_image::texture_to_image;
|
use crate::texture_to_image::texture_to_image;
|
||||||
use crate::Harness;
|
use eframe::epaint::TextureId;
|
||||||
use egui_wgpu::wgpu::{Backends, InstanceDescriptor, StoreOp, TextureFormat};
|
use egui::TexturesDelta;
|
||||||
use egui_wgpu::{wgpu, ScreenDescriptor};
|
use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat};
|
||||||
|
use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
use std::sync::Arc;
|
||||||
use wgpu::Maintain;
|
use wgpu::Maintain;
|
||||||
|
|
||||||
/// Utility to render snapshots from a [`Harness`] using [`egui_wgpu`].
|
// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506
|
||||||
pub struct TestRenderer {
|
pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
|
||||||
device: wgpu::Device,
|
egui_wgpu::WgpuSetup::CreateNew {
|
||||||
queue: wgpu::Queue,
|
supported_backends: Backends::all(),
|
||||||
dithering: bool,
|
device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()),
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestRenderer {
|
pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState {
|
||||||
|
let instance = match &setup {
|
||||||
|
WgpuSetup::Existing { instance, .. } => instance.clone(),
|
||||||
|
WgpuSetup::CreateNew { .. } => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
pollster::block_on(egui_wgpu::RenderState::create(
|
||||||
|
&egui_wgpu::WgpuConfiguration {
|
||||||
|
wgpu_setup: setup,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&instance,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.expect("Failed to create render state")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility to render snapshots from a [`crate::Harness`] using [`egui_wgpu`].
|
||||||
|
pub struct WgpuTestRenderer {
|
||||||
|
render_state: RenderState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WgpuTestRenderer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestRenderer {
|
impl WgpuTestRenderer {
|
||||||
/// Create a new [`TestRenderer`] using a default [`wgpu::Instance`].
|
/// Create a new [`WgpuTestRenderer`] with the default setup.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let instance = wgpu::Instance::new(InstanceDescriptor::default());
|
|
||||||
|
|
||||||
let adapters = instance.enumerate_adapters(Backends::all());
|
|
||||||
let adapter = adapters.first().expect("No adapter found");
|
|
||||||
|
|
||||||
let (device, queue) = pollster::block_on(adapter.request_device(
|
|
||||||
&wgpu::DeviceDescriptor {
|
|
||||||
label: Some("Egui Device"),
|
|
||||||
memory_hints: Default::default(),
|
|
||||||
required_limits: Default::default(),
|
|
||||||
required_features: Default::default(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.expect("Failed to create device");
|
|
||||||
|
|
||||||
Self::create(device, queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`TestRenderer`] using the provided [`wgpu::Device`] and [`wgpu::Queue`].
|
|
||||||
pub fn create(device: wgpu::Device, queue: wgpu::Queue) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
device,
|
render_state: create_render_state(default_wgpu_setup()),
|
||||||
queue,
|
|
||||||
dithering: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable or disable dithering.
|
/// Create a new [`WgpuTestRenderer`] with the given setup.
|
||||||
|
pub fn from_setup(setup: WgpuSetup) -> Self {
|
||||||
|
Self {
|
||||||
|
render_state: create_render_state(setup),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`WgpuTestRenderer`] from an existing [`RenderState`].
|
||||||
///
|
///
|
||||||
/// Disabled by default.
|
/// # Panics
|
||||||
#[inline]
|
/// Panics if the [`RenderState`] has been used before.
|
||||||
pub fn with_dithering(mut self, dithering: bool) -> Self {
|
pub fn from_render_state(render_state: RenderState) -> Self {
|
||||||
self.dithering = dithering;
|
assert!(
|
||||||
self
|
render_state
|
||||||
|
.renderer
|
||||||
|
.read()
|
||||||
|
.texture(&TextureId::Managed(0))
|
||||||
|
.is_none(),
|
||||||
|
"The RenderState passed in has been used before, pass in a fresh RenderState instead."
|
||||||
|
);
|
||||||
|
Self { render_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::TestRenderer for WgpuTestRenderer {
|
||||||
|
#[cfg(feature = "eframe")]
|
||||||
|
fn setup_eframe(&self, cc: &mut eframe::CreationContext<'_>, frame: &mut eframe::Frame) {
|
||||||
|
cc.wgpu_render_state = Some(self.render_state.clone());
|
||||||
|
frame.wgpu_render_state = Some(self.render_state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the [`Harness`] and return the resulting image.
|
fn handle_delta(&mut self, delta: &TexturesDelta) {
|
||||||
pub fn render<State>(&self, harness: &Harness<'_, State>) -> RgbaImage {
|
let mut renderer = self.render_state.renderer.write();
|
||||||
// We need to create a new renderer each time we render, since the renderer stores
|
for (id, image) in &delta.set {
|
||||||
// textures related to the Harnesses' egui Context.
|
renderer.update_texture(
|
||||||
// Calling the renderer from different Harnesses would cause problems if we store the renderer.
|
&self.render_state.device,
|
||||||
let mut renderer = egui_wgpu::Renderer::new(
|
&self.render_state.queue,
|
||||||
&self.device,
|
*id,
|
||||||
TextureFormat::Rgba8Unorm,
|
image,
|
||||||
None,
|
);
|
||||||
1,
|
|
||||||
self.dithering,
|
|
||||||
);
|
|
||||||
|
|
||||||
for delta in &harness.texture_deltas {
|
|
||||||
for (id, image_delta) in &delta.set {
|
|
||||||
renderer.update_texture(&self.device, &self.queue, *id, image_delta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut encoder = self
|
/// Render the [`crate::Harness`] and return the resulting image.
|
||||||
.device
|
fn render(
|
||||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
&mut self,
|
||||||
label: Some("Egui Command Encoder"),
|
ctx: &egui::Context,
|
||||||
});
|
output: &egui::FullOutput,
|
||||||
|
) -> Result<RgbaImage, String> {
|
||||||
|
let mut renderer = self.render_state.renderer.write();
|
||||||
|
|
||||||
let size = harness.ctx.screen_rect().size() * harness.ctx.pixels_per_point();
|
let mut encoder =
|
||||||
|
self.render_state
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("Egui Command Encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let size = ctx.screen_rect().size() * ctx.pixels_per_point();
|
||||||
let screen = ScreenDescriptor {
|
let screen = ScreenDescriptor {
|
||||||
pixels_per_point: harness.ctx.pixels_per_point(),
|
pixels_per_point: ctx.pixels_per_point(),
|
||||||
size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
|
size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
|
||||||
};
|
};
|
||||||
|
|
||||||
let tessellated = harness.ctx.tessellate(
|
let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point());
|
||||||
harness.output().shapes.clone(),
|
|
||||||
harness.ctx.pixels_per_point(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let user_buffers = renderer.update_buffers(
|
let user_buffers = renderer.update_buffers(
|
||||||
&self.device,
|
&self.render_state.device,
|
||||||
&self.queue,
|
&self.render_state.queue,
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&tessellated,
|
&tessellated,
|
||||||
&screen,
|
&screen,
|
||||||
);
|
);
|
||||||
|
|
||||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
let texture = self
|
||||||
label: Some("Egui Texture"),
|
.render_state
|
||||||
size: wgpu::Extent3d {
|
.device
|
||||||
width: screen.size_in_pixels[0],
|
.create_texture(&wgpu::TextureDescriptor {
|
||||||
height: screen.size_in_pixels[1],
|
label: Some("Egui Texture"),
|
||||||
depth_or_array_layers: 1,
|
size: wgpu::Extent3d {
|
||||||
},
|
width: screen.size_in_pixels[0],
|
||||||
mip_level_count: 1,
|
height: screen.size_in_pixels[1],
|
||||||
sample_count: 1,
|
depth_or_array_layers: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
},
|
||||||
format: TextureFormat::Rgba8Unorm,
|
mip_level_count: 1,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
sample_count: 1,
|
||||||
view_formats: &[],
|
dimension: wgpu::TextureDimension::D2,
|
||||||
});
|
format: TextureFormat::Rgba8Unorm,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
|
@ -141,11 +171,16 @@ impl TestRenderer {
|
||||||
renderer.render(&mut pass, &tessellated, &screen);
|
renderer.render(&mut pass, &tessellated, &screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.queue
|
self.render_state
|
||||||
|
.queue
|
||||||
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
|
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
|
||||||
|
|
||||||
self.device.poll(Maintain::Wait);
|
self.render_state.device.poll(Maintain::Wait);
|
||||||
|
|
||||||
texture_to_image(&self.device, &self.queue, &texture)
|
Ok(texture_to_image(
|
||||||
|
&self.render_state.device,
|
||||||
|
&self.render_state.queue,
|
||||||
|
&texture,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,5 +41,5 @@ fn image_failed() {
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
|
|
||||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||||
harness.wgpu_snapshot("image_snapshots");
|
harness.snapshot("image_snapshots");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ fn test_shrink() {
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
|
|
||||||
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
|
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
|
||||||
harness.wgpu_snapshot("test_shrink");
|
harness.snapshot("test_shrink");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue