Skip to content

Commit

Permalink
Add Harness::new_ui, Harness::fit_contents (#5301)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lucasmerlin authored Nov 1, 2024
1 parent 21826be commit ad14bf2
Show file tree
Hide file tree
Showing 17 changed files with 242 additions and 38 deletions.
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ dependencies = [
"document-features",
"egui",
"egui-wgpu",
"egui_kittest",
"image",
"kittest",
"pollster",
Expand Down
15 changes: 6 additions & 9 deletions crates/egui_demo_lib/src/demo/widget_gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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");
}
Expand Down
36 changes: 32 additions & 4 deletions crates/egui_demo_lib/src/rendering_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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:#?}");
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions crates/egui_demo_lib/tests/snapshots/widget_gallery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions crates/egui_kittest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
74 changes: 74 additions & 0 deletions crates/egui_kittest/src/app_kind.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 29 additions & 7 deletions crates/egui_kittest/src/builder.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Expand All @@ -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
Expand All @@ -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)))
}
}
Loading

0 comments on commit ad14bf2

Please sign in to comment.