1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

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:
lucasmerlin
2024-11-01 18:30:40 +01:00
committed by GitHub
parent 21826bec18
commit ad14bf2490
17 changed files with 242 additions and 38 deletions

View File

@@ -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
}
}

View File

@@ -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)))
}
}

View File

@@ -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

View File

@@ -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,
},
})],