Add `Harness::new_ui`, `Harness::fit_contents` (#5301)
This adds a `Harness::new_ui`, which accepts a Ui closure and shows the ui in a central panel. One big benefit is that this allows us to add a fit_contents method that can run the ui closure with a sizing pass and resize the "screen" based on the content size. I also used this to add a snapshot test for the rendering_test at different scales.
This commit is contained in:
parent
21826bec18
commit
ad14bf2490
|
|
@ -1364,6 +1364,7 @@ dependencies = [
|
|||
"document-features",
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"egui_kittest",
|
||||
"image",
|
||||
"kittest",
|
||||
"pollster",
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ fn doc_link_label_with_crate<'a>(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::View;
|
||||
use egui::{CentralPanel, Context, Vec2};
|
||||
use egui::Vec2;
|
||||
use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
|
|
@ -300,15 +300,12 @@ mod tests {
|
|||
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let app = |ctx: &Context| {
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
demo.ui(ui);
|
||||
});
|
||||
};
|
||||
let harness = Harness::builder()
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.with_dpi(2.0)
|
||||
.build(app);
|
||||
.build_ui(|ui| demo.ui(ui));
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
harness.wgpu_snapshot("widget_gallery");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -435,10 +435,7 @@ fn pixel_test_strokes(ui: &mut Ui) {
|
|||
let thickness_pixels = thickness_pixels as f32;
|
||||
let thickness_points = thickness_pixels / pixels_per_point;
|
||||
let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32;
|
||||
let size_pixels = vec2(
|
||||
ui.available_width(),
|
||||
num_squares as f32 + thickness_pixels * 2.0,
|
||||
);
|
||||
let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
|
|
@ -680,3 +677,34 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
|
|||
(left.a() as f32 * right.a() as f32 / 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ColorTest;
|
||||
use egui::vec2;
|
||||
|
||||
#[test]
|
||||
pub fn rendering_test() {
|
||||
let mut errors = vec![];
|
||||
for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] {
|
||||
let mut color_test = ColorTest::default();
|
||||
let mut harness = egui_kittest::Harness::builder()
|
||||
.with_size(vec2(2000.0, 2000.0))
|
||||
.with_pixels_per_point(dpi)
|
||||
.build_ui(|ui| {
|
||||
color_test.ui(ui);
|
||||
});
|
||||
|
||||
//harness.set_size(harness.ctx.used_size());
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
|
||||
if let Err(err) = result {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(errors.is_empty(), "Errors: {errors:#?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:023eaa363b42ec24ae845dc2ca9ff271a0bd47217e625785d3716044ecfa7a64
|
||||
size 278444
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d81f618e54176b1c43b710121f249e13ce29827fbea3451827ab62229006677e
|
||||
size 378603
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d8eca6d5555ef779233175615b877fb91318b4a09a37e5cfbe71973d56f4caf
|
||||
size 465907
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4768804f57dfc54c5f6b84a2686038b8d630a28c7e928ae044d5b2ce8377e2cd
|
||||
size 538775
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fcee0e0302f33d681348d62bee3b548beb494c6dd1fa3454586986e0b699e162
|
||||
size 572403
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:254a8dff0b1d4b74971fd3bd4044c4ec0ce49412a95e98419a14dc55b32a4fc9
|
||||
size 663272
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5dc632962f8894c4f20a48c9b9e57d60470f3f83ef7f19d05854dba718610a2f
|
||||
size 161820
|
||||
oid sha256:c069ef4f86beeeafd8686f30fc914bedd7e7e7ec38fd96e9a46ac6b31308c43f
|
||||
size 160883
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ dify = { workspace = true, optional = true }
|
|||
document-features = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||
wgpu = { workspace = true, features = ["metal"] }
|
||||
image = { workspace = true, features = ["png"] }
|
||||
egui = { workspace = true, features = ["default_fonts"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
use egui::Frame;
|
||||
|
||||
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
|
||||
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;
|
||||
|
||||
pub(crate) enum AppKind<'a> {
|
||||
Context(AppKindContext<'a>),
|
||||
Ui(AppKindUi<'a>),
|
||||
}
|
||||
|
||||
// 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> AppKind<'a> {
|
||||
pub fn run(&mut self, ctx: &egui::Context) -> Option<egui::Response> {
|
||||
match self {
|
||||
AppKind::Context(f) => {
|
||||
f(ctx);
|
||||
None
|
||||
}
|
||||
AppKind::Ui(f) => Some(Self::run_ui(f, ctx, false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_sizing_pass(&mut self, ctx: &egui::Context) -> Option<egui::Response> {
|
||||
match self {
|
||||
AppKind::Context(f) => {
|
||||
f(ctx);
|
||||
None
|
||||
}
|
||||
AppKind::Ui(f) => Some(Self::run_ui(f, ctx, true)),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_ui(f: &mut AppKindUi<'a>, ctx: &egui::Context, sizing_pass: bool) -> egui::Response {
|
||||
egui::CentralPanel::default()
|
||||
.frame(Frame::none())
|
||||
.show(ctx, |ui| {
|
||||
let mut builder = egui::UiBuilder::new();
|
||||
if sizing_pass {
|
||||
builder.sizing_pass = true;
|
||||
}
|
||||
ui.scope_builder(builder, |ui| {
|
||||
Frame::central_panel(ui.style())
|
||||
.outer_margin(8.0)
|
||||
.inner_margin(0.0)
|
||||
.show(ui, |ui| f(ui));
|
||||
})
|
||||
.response
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
use crate::app_kind::AppKind;
|
||||
use crate::Harness;
|
||||
use egui::{Pos2, Rect, Vec2};
|
||||
|
||||
/// Builder for [`Harness`].
|
||||
pub struct HarnessBuilder {
|
||||
pub(crate) screen_rect: Rect,
|
||||
pub(crate) dpi: f32,
|
||||
pub(crate) pixels_per_point: f32,
|
||||
}
|
||||
|
||||
impl Default for HarnessBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
|
||||
dpi: 1.0,
|
||||
pixels_per_point: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,16 +27,18 @@ impl HarnessBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the DPI of the window.
|
||||
/// Set the `pixels_per_point` of the window.
|
||||
#[inline]
|
||||
pub fn with_dpi(mut self, dpi: f32) -> Self {
|
||||
self.dpi = dpi;
|
||||
pub fn with_pixels_per_point(mut self, pixels_per_point: f32) -> Self {
|
||||
self.pixels_per_point = pixels_per_point;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given app closure.
|
||||
///
|
||||
/// The ui closure will immediately be called once to create the initial ui.
|
||||
/// The app closure will immediately be called once to create the initial ui.
|
||||
///
|
||||
/// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
|
|
@ -50,6 +53,25 @@ impl HarnessBuilder {
|
|||
/// });
|
||||
/// ```
|
||||
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
|
||||
Harness::from_builder(&self, app)
|
||||
Harness::from_builder(&self, AppKind::Context(Box::new(app)))
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given ui closure.
|
||||
///
|
||||
/// The ui closure will immediately be called once to create the initial ui.
|
||||
///
|
||||
/// If you need to create Windows / Panels, you can use [`HarnessBuilder::build`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui_kittest::Harness;
|
||||
/// let mut harness = Harness::builder()
|
||||
/// .with_size(egui::Vec2::new(300.0, 200.0))
|
||||
/// .build_ui(|ui| {
|
||||
/// ui.label("Hello, world!");
|
||||
/// });
|
||||
/// ```
|
||||
pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> {
|
||||
Harness::from_builder(&self, AppKind::Ui(Box::new(app)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mod snapshot;
|
|||
#[cfg(feature = "snapshot")]
|
||||
pub use snapshot::*;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
mod app_kind;
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod texture_to_image;
|
||||
#[cfg(feature = "wgpu")]
|
||||
|
|
@ -19,6 +20,7 @@ pub mod wgpu;
|
|||
pub use kittest;
|
||||
use std::mem;
|
||||
|
||||
use crate::app_kind::AppKind;
|
||||
use crate::event::EventState;
|
||||
pub use builder::*;
|
||||
use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId};
|
||||
|
|
@ -32,8 +34,9 @@ pub struct Harness<'a> {
|
|||
kittest: kittest::State,
|
||||
output: egui::FullOutput,
|
||||
texture_deltas: Vec<TexturesDelta>,
|
||||
update_fn: Box<dyn FnMut(&egui::Context) + 'a>,
|
||||
app: AppKind<'a>,
|
||||
event_state: EventState,
|
||||
response: Option<egui::Response>,
|
||||
}
|
||||
|
||||
impl<'a> Debug for Harness<'a> {
|
||||
|
|
@ -43,10 +46,7 @@ impl<'a> Debug for Harness<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Harness<'a> {
|
||||
pub(crate) fn from_builder(
|
||||
builder: &HarnessBuilder,
|
||||
mut app: impl FnMut(&egui::Context) + 'a,
|
||||
) -> Self {
|
||||
pub(crate) fn from_builder(builder: &HarnessBuilder, mut app: AppKind<'a>) -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
let mut input = egui::RawInput {
|
||||
|
|
@ -54,14 +54,18 @@ impl<'a> Harness<'a> {
|
|||
..Default::default()
|
||||
};
|
||||
let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap();
|
||||
viewport.native_pixels_per_point = Some(builder.dpi);
|
||||
viewport.native_pixels_per_point = Some(builder.pixels_per_point);
|
||||
|
||||
let mut response = None;
|
||||
|
||||
// We need to run egui for a single frame so that the AccessKit state can be initialized
|
||||
// and users can immediately start querying for widgets.
|
||||
let mut output = ctx.run(input.clone(), &mut app);
|
||||
let mut output = ctx.run(input.clone(), |ctx| {
|
||||
response = app.run(ctx);
|
||||
});
|
||||
|
||||
let mut harness = Self {
|
||||
update_fn: Box::new(app),
|
||||
app,
|
||||
ctx,
|
||||
input,
|
||||
kittest: kittest::State::new(
|
||||
|
|
@ -73,6 +77,7 @@ impl<'a> Harness<'a> {
|
|||
),
|
||||
texture_deltas: vec![mem::take(&mut output.textures_delta)],
|
||||
output,
|
||||
response,
|
||||
event_state: EventState::default(),
|
||||
};
|
||||
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
||||
|
|
@ -86,7 +91,9 @@ impl<'a> Harness<'a> {
|
|||
|
||||
/// Create a new Harness with the given app closure.
|
||||
///
|
||||
/// The ui closure will immediately be called once to create the initial ui.
|
||||
/// The app closure will immediately be called once to create the initial ui.
|
||||
///
|
||||
/// If you don't need to create Windows / Panels, you can use [`Harness::new_ui`] instead.
|
||||
///
|
||||
/// If you e.g. want to customize the size of the window, you can use [`Harness::builder`].
|
||||
///
|
||||
|
|
@ -104,6 +111,25 @@ impl<'a> Harness<'a> {
|
|||
Self::builder().build(app)
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given ui closure.
|
||||
///
|
||||
/// The ui closure will immediately be called once to create the initial ui.
|
||||
///
|
||||
/// If you need to create Windows / Panels, you can use [`Harness::new`] instead.
|
||||
///
|
||||
/// If you e.g. want to customize the size of the ui, you can use [`Harness::builder`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui_kittest::Harness;
|
||||
/// let mut harness = Harness::new_ui(|ui| {
|
||||
/// ui.label("Hello, world!");
|
||||
/// });
|
||||
/// ```
|
||||
pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self {
|
||||
Self::builder().build_ui(app)
|
||||
}
|
||||
|
||||
/// Set the size of the window.
|
||||
/// Note: If you only want to set the size once at the beginning,
|
||||
/// prefer using [`HarnessBuilder::with_size`].
|
||||
|
|
@ -113,25 +139,35 @@ impl<'a> Harness<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the DPI of the window.
|
||||
/// Note: If you only want to set the DPI once at the beginning,
|
||||
/// prefer using [`HarnessBuilder::with_dpi`].
|
||||
/// Set the `pixels_per_point` of the window.
|
||||
/// Note: If you only want to set the `pixels_per_point` once at the beginning,
|
||||
/// prefer using [`HarnessBuilder::with_pixels_per_point`].
|
||||
#[inline]
|
||||
pub fn set_dpi(&mut self, dpi: f32) -> &mut Self {
|
||||
self.ctx.set_pixels_per_point(dpi);
|
||||
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) -> &mut Self {
|
||||
self.ctx.set_pixels_per_point(pixels_per_point);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run a frame.
|
||||
/// This will call the app closure with the current context and update the Harness.
|
||||
pub fn step(&mut self) {
|
||||
self._step(false);
|
||||
}
|
||||
|
||||
fn _step(&mut self, sizing_pass: bool) {
|
||||
for event in self.kittest.take_events() {
|
||||
if let Some(event) = self.event_state.kittest_event_to_egui(event) {
|
||||
self.input.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = self.ctx.run(self.input.take(), self.update_fn.as_mut());
|
||||
let mut output = self.ctx.run(self.input.take(), |ctx| {
|
||||
if sizing_pass {
|
||||
self.response = self.app.run_sizing_pass(ctx);
|
||||
} else {
|
||||
self.response = self.app.run(ctx);
|
||||
}
|
||||
});
|
||||
self.kittest.update(
|
||||
output
|
||||
.platform_output
|
||||
|
|
@ -144,6 +180,16 @@ impl<'a> Harness<'a> {
|
|||
self.output = output;
|
||||
}
|
||||
|
||||
/// Resize the test harness to fit the contents. This only works when creating the Harness via
|
||||
/// [`Harness::new_ui`] or [`HarnessBuilder::build_ui`].
|
||||
pub fn fit_contents(&mut self) {
|
||||
self._step(true);
|
||||
if let Some(response) = &self.response {
|
||||
self.set_size(response.rect.size());
|
||||
}
|
||||
self.run();
|
||||
}
|
||||
|
||||
/// Run a few frames.
|
||||
/// This will soon be changed to run the app until it is "stable", meaning
|
||||
/// - all animations are done
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ impl TestRenderer {
|
|||
view: &texture_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7008bdb595a19782c4f724bed363e51bd93121f5211186aa0e8014c8ba1007c2
|
||||
size 3005
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
fn test_shrink() {
|
||||
let mut harness = Harness::new_ui(|ui| {
|
||||
ui.label("Hello, world!");
|
||||
ui.separator();
|
||||
ui.label("This is a test");
|
||||
});
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
harness.wgpu_snapshot("test_shrink");
|
||||
}
|
||||
Loading…
Reference in New Issue