mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
Merge branch 'main' into ime-preedit-visuals
This commit is contained in:
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.1 - 2026-03-27
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
use egui::Frame;
|
||||
|
||||
type AppKindContextState<'a, State> = Box<dyn FnMut(&egui::Context, &mut State) + 'a>;
|
||||
type AppKindUiState<'a, State> = Box<dyn FnMut(&mut egui::Ui, &mut State) + 'a>;
|
||||
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + '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.
|
||||
/// 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) struct AppKindEframe<State> {
|
||||
pub get_app: fn(&mut State) -> &mut dyn eframe::App,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub take_app: fn(State) -> Box<dyn eframe::App>,
|
||||
pub frame: eframe::Frame,
|
||||
}
|
||||
|
||||
pub(crate) enum AppKind<'a, State> {
|
||||
Context(AppKindContext<'a>),
|
||||
Ui(AppKindUi<'a>),
|
||||
ContextState(AppKindContextState<'a, State>),
|
||||
UiState(AppKindUiState<'a, State>),
|
||||
#[cfg(feature = "eframe")]
|
||||
Eframe(AppKindEframe<'a, State>),
|
||||
Eframe(AppKindEframe<State>),
|
||||
}
|
||||
|
||||
impl<State> AppKind<'_, State> {
|
||||
@@ -29,27 +30,11 @@ impl<State> AppKind<'_, State> {
|
||||
sizing_pass: bool,
|
||||
) -> Option<egui::Response> {
|
||||
match self {
|
||||
AppKind::Context(f) => {
|
||||
debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass");
|
||||
f(ui);
|
||||
None
|
||||
}
|
||||
AppKind::ContextState(f) => {
|
||||
debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass");
|
||||
f(ui, state);
|
||||
None
|
||||
}
|
||||
#[cfg(feature = "eframe")]
|
||||
AppKind::Eframe((get_app, frame)) => {
|
||||
AppKind::Eframe(AppKindEframe { get_app, frame, .. }) => {
|
||||
let app = get_app(state);
|
||||
|
||||
app.logic(ui, frame);
|
||||
|
||||
#[expect(deprecated)]
|
||||
app.update(ui, frame);
|
||||
|
||||
app.ui(ui, frame);
|
||||
|
||||
None
|
||||
}
|
||||
kind_ui => Some(kind_ui.run_ui(ui, state, sizing_pass)),
|
||||
@@ -74,8 +59,9 @@ impl<State> AppKind<'_, State> {
|
||||
.show(ui, |ui| match self {
|
||||
AppKind::Ui(f) => f(ui),
|
||||
AppKind::UiState(f) => f(ui, state),
|
||||
_ => unreachable!(
|
||||
"run_ui should only be called with AppKind::Ui or AppKind UiState"
|
||||
#[cfg(feature = "eframe")]
|
||||
AppKind::Eframe(_) => unreachable!(
|
||||
"run_ui should only be called with AppKind::Ui or AppKind::UiState"
|
||||
),
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::app_kind::AppKind;
|
||||
#[cfg(feature = "eframe")]
|
||||
use crate::app_kind::AppKindEframe;
|
||||
use crate::{Harness, LazyRenderer, TestRenderer};
|
||||
use egui::{Pos2, Rect, Vec2};
|
||||
use std::marker::PhantomData;
|
||||
@@ -159,46 +161,10 @@ impl<State> HarnessBuilder<State> {
|
||||
self.renderer(crate::wgpu::WgpuTestRenderer::from_setup(setup))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui::CentralPanel;
|
||||
/// # use egui_kittest::{Harness, kittest::Queryable};
|
||||
/// let checked = false;
|
||||
/// let mut harness = Harness::builder()
|
||||
/// .with_size(egui::Vec2::new(300.0, 200.0))
|
||||
/// .build_state(|ctx, checked| {
|
||||
/// CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.checkbox(checked, "Check me!");
|
||||
/// });
|
||||
/// }, checked);
|
||||
///
|
||||
/// harness.get_by_label("Check me!").click();
|
||||
/// harness.run();
|
||||
///
|
||||
/// assert_eq!(*harness.state(), true);
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "use `build_ui_state` instead"]
|
||||
pub fn build_state<'a>(
|
||||
self,
|
||||
app: impl FnMut(&egui::Context, &mut State) + 'a,
|
||||
state: State,
|
||||
) -> Harness<'a, State> {
|
||||
Harness::from_builder(self, AppKind::ContextState(Box::new(app)), state, None)
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given ui closure and a state.
|
||||
///
|
||||
/// 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, kittest::Queryable};
|
||||
@@ -232,7 +198,7 @@ impl<State> HarnessBuilder<State> {
|
||||
build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State,
|
||||
) -> Harness<'a, State>
|
||||
where
|
||||
State: eframe::App,
|
||||
State: eframe::App + 'static,
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -243,43 +209,21 @@ impl<State> HarnessBuilder<State> {
|
||||
|
||||
let app = build(&mut cc);
|
||||
|
||||
let kind = AppKind::Eframe((|state| state, frame));
|
||||
let kind = AppKind::Eframe(AppKindEframe {
|
||||
get_app: |state| state,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
take_app: |state| Box::new(state),
|
||||
frame,
|
||||
});
|
||||
Harness::from_builder(self, kind, app, Some(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
impl HarnessBuilder {
|
||||
/// Create a new Harness with the given app closure.
|
||||
///
|
||||
/// 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
|
||||
/// # use egui::CentralPanel;
|
||||
/// # use egui_kittest::{Harness, kittest::Queryable};
|
||||
/// let mut harness = Harness::builder()
|
||||
/// .with_size(egui::Vec2::new(300.0, 200.0))
|
||||
/// .build(|ctx| {
|
||||
/// CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.label("Hello, world!");
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
#[deprecated = "use `build_ui` instead"]
|
||||
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
|
||||
Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None)
|
||||
}
|
||||
|
||||
/// 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, kittest::Queryable};
|
||||
|
||||
@@ -77,7 +77,7 @@ fn load_config() -> Config {
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(config_str) => match toml::from_str(&config_str) {
|
||||
Ok(config) => config,
|
||||
Err(e) => panic!("Failed to parse {}: {e}", &config_path.display()),
|
||||
Err(e) => panic!("Failed to parse {}: {e}", config_path.display()),
|
||||
},
|
||||
Err(err) => {
|
||||
panic!("Failed to read {}: {}", config_path.display(), err);
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Display for ExceededMaxStepsError {
|
||||
|
||||
/// The test Harness. This contains everything needed to run the test.
|
||||
///
|
||||
/// Create a new Harness using [`Harness::new`] or [`Harness::builder`].
|
||||
/// Create a new Harness using [`Harness::new_ui`] or [`Harness::builder`].
|
||||
///
|
||||
/// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure.
|
||||
/// In _most cases_ it should be fine to just store the state in the closure itself.
|
||||
@@ -185,43 +185,10 @@ impl<'a, State> Harness<'a, State> {
|
||||
HarnessBuilder::default()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui::CentralPanel;
|
||||
/// # use egui_kittest::{Harness, kittest::Queryable};
|
||||
/// let mut checked = false;
|
||||
/// let mut harness = Harness::new_state(|ctx, checked| {
|
||||
/// CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.checkbox(checked, "Check me!");
|
||||
/// });
|
||||
/// }, checked);
|
||||
///
|
||||
/// harness.get_by_label("Check me!").click();
|
||||
/// harness.run();
|
||||
///
|
||||
/// assert_eq!(*harness.state(), true);
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "use `new_ui_state` instead"]
|
||||
pub fn new_state(app: impl FnMut(&egui::Context, &mut State) + 'a, state: State) -> Self {
|
||||
#[expect(deprecated)]
|
||||
Self::builder().build_state(app, state)
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given ui closure and a state.
|
||||
///
|
||||
/// 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
|
||||
@@ -247,7 +214,7 @@ impl<'a, State> Harness<'a, State> {
|
||||
#[track_caller]
|
||||
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
|
||||
where
|
||||
State: eframe::App,
|
||||
State: eframe::App + 'static,
|
||||
{
|
||||
Self::builder().build_eframe(builder)
|
||||
}
|
||||
@@ -312,11 +279,7 @@ impl<'a, State> Harness<'a, State> {
|
||||
/// Calculate the rect that includes all popups and tooltips.
|
||||
fn compute_total_rect_with_popups(&self) -> Option<Rect> {
|
||||
// Start with the standard response rect
|
||||
let mut used = if let Some(response) = self.response.as_ref() {
|
||||
response.rect
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let mut used = self.response.as_ref()?.rect;
|
||||
|
||||
// Add all visible areas from other orders (popups, tooltips, etc.)
|
||||
self.ctx.memory(|mem| {
|
||||
@@ -493,6 +456,11 @@ impl<'a, State> Harness<'a, State> {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
/// Consume the harness and return the state.
|
||||
pub fn into_state(self) -> State {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Queue an event to be processed in the next frame.
|
||||
pub fn event(&self, event: egui::Event) {
|
||||
self.queued_events.lock().push(EventType::Event(event));
|
||||
@@ -715,47 +683,118 @@ impl<'a, State> Harness<'a, State> {
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Use `Harness::root` instead."]
|
||||
pub fn node(&self) -> Node<'_> {
|
||||
self.root()
|
||||
/// Spawn a real native eframe window running this harness's app, reusing its [`egui::Context`].
|
||||
///
|
||||
/// Blocks until the window is closed.
|
||||
///
|
||||
/// Useful for interactively debugging a failing test: add a call to this before the failing
|
||||
/// assertion to poke at the UI yourself.
|
||||
///
|
||||
/// # macOS: must be called on the main thread
|
||||
/// `AppKit` requires UI work to happen on the main thread, but by default cargo's test harness
|
||||
/// runs each test on a spawned worker thread, so this function will panic on macOS unless
|
||||
/// you opt out of the default harness.
|
||||
///
|
||||
/// To fix this, disable the default libtest harness for your test target and run tests on
|
||||
/// the main thread yourself. In `Cargo.toml`:
|
||||
///
|
||||
/// ```toml
|
||||
/// [[test]]
|
||||
/// name = "your_test"
|
||||
/// harness = false
|
||||
/// ```
|
||||
///
|
||||
/// Then write a `fn main()` in the test file that invokes your test directly.
|
||||
///
|
||||
/// See also: <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field>
|
||||
#[cfg(all(feature = "eframe", not(target_arch = "wasm32")))]
|
||||
#[deprecated = "Only for debugging, don't commit this."]
|
||||
pub fn spawn_eframe_app(self)
|
||||
where
|
||||
'a: 'static,
|
||||
State: 'static,
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// AppKit requires UI work to happen on the main thread, but by default cargo's
|
||||
// test harness runs each test on a spawned worker thread.
|
||||
#[expect(unsafe_code)]
|
||||
// SAFETY: `pthread_main_np` is a thread-safe libc query with no arguments.
|
||||
let is_main_thread = unsafe {
|
||||
unsafe extern "C" {
|
||||
fn pthread_main_np() -> std::ffi::c_int;
|
||||
}
|
||||
pthread_main_np() != 0
|
||||
};
|
||||
assert!(
|
||||
is_main_thread,
|
||||
"spawn_eframe_app must be called on the main thread on macOS, \
|
||||
but the default `cargo test` harness runs each test on a worker thread.\n\
|
||||
\n\
|
||||
To fix this, disable the default libtest harness for your test target and run \
|
||||
tests on the main thread yourself. In Cargo.toml:\n\
|
||||
\n\
|
||||
[[test]]\n\
|
||||
name = \"your_test\"\n\
|
||||
harness = false\n\
|
||||
\n\
|
||||
Then write a `fn main()` in the test file that invokes your test directly.\n\
|
||||
\n\
|
||||
See: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field"
|
||||
);
|
||||
}
|
||||
|
||||
struct UiApp {
|
||||
f: Box<dyn FnMut(&mut egui::Ui)>,
|
||||
}
|
||||
|
||||
impl eframe::App for UiApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
(self.f)(ui);
|
||||
}
|
||||
}
|
||||
|
||||
struct UiStateApp<State> {
|
||||
f: Box<dyn FnMut(&mut egui::Ui, &mut State)>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl<State: 'static> eframe::App for UiStateApp<State> {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let Self { f, state } = self;
|
||||
f(ui, state);
|
||||
}
|
||||
}
|
||||
|
||||
use crate::app_kind::AppKindEframe;
|
||||
|
||||
let Self {
|
||||
ctx, state, app, ..
|
||||
} = self;
|
||||
|
||||
let eframe_app: Box<dyn eframe::App> = match app {
|
||||
AppKind::Ui(f) => Box::new(UiApp { f }),
|
||||
AppKind::UiState(f) => Box::new(UiStateApp { f, state }),
|
||||
AppKind::Eframe(AppKindEframe { take_app, .. }) => take_app(state),
|
||||
};
|
||||
|
||||
eframe::run_native_ext(
|
||||
"egui_kittest",
|
||||
eframe::NativeOptions::default(),
|
||||
Some(ctx),
|
||||
Box::new(|_cc| Ok(eframe_app)),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities for stateless harnesses.
|
||||
impl<'a> Harness<'a> {
|
||||
/// Create a new Harness with the given app closure.
|
||||
/// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app.
|
||||
///
|
||||
/// 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`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui::CentralPanel;
|
||||
/// # use egui_kittest::Harness;
|
||||
/// let mut harness = Harness::new(|ctx| {
|
||||
/// CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.label("Hello, world!");
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "use `new_ui` instead"]
|
||||
pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self {
|
||||
#[expect(deprecated)]
|
||||
Self::builder().build(app)
|
||||
}
|
||||
|
||||
/// Create a new Harness with the given ui closure.
|
||||
/// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app.
|
||||
///
|
||||
/// 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
|
||||
|
||||
@@ -54,11 +54,6 @@ impl Node<'_> {
|
||||
self.click_button(PointerButton::Primary);
|
||||
}
|
||||
|
||||
#[deprecated = "Use `click()` instead."]
|
||||
pub fn simulate_click(&self) {
|
||||
self.click();
|
||||
}
|
||||
|
||||
pub fn click_secondary(&self) {
|
||||
self.click_button(PointerButton::Secondary);
|
||||
}
|
||||
@@ -130,28 +125,6 @@ impl Node<'_> {
|
||||
}));
|
||||
}
|
||||
|
||||
#[deprecated = "Use `Harness::key_down` instead."]
|
||||
pub fn key_down(&self, key: egui::Key) {
|
||||
self.event(egui::Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
modifiers: Modifiers::default(),
|
||||
repeat: false,
|
||||
physical_key: None,
|
||||
});
|
||||
}
|
||||
|
||||
#[deprecated = "Use `Harness::key_up` instead."]
|
||||
pub fn key_up(&self, key: egui::Key) {
|
||||
self.event(egui::Event::Key {
|
||||
key,
|
||||
pressed: false,
|
||||
modifiers: Modifiers::default(),
|
||||
repeat: false,
|
||||
physical_key: None,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn type_text(&self, text: &str) {
|
||||
self.event(egui::Event::Text(text.to_owned()));
|
||||
}
|
||||
|
||||
@@ -173,8 +173,9 @@ impl SnapshotOptions {
|
||||
/// The default is `0.6` (which is enough for most egui tests to pass across different
|
||||
/// wgpu backends).
|
||||
#[inline]
|
||||
pub fn threshold(mut self, threshold: impl Into<f32>) -> Self {
|
||||
self.threshold = threshold.into();
|
||||
pub fn threshold(mut self, threshold: impl Into<OsThreshold<f32>>) -> Self {
|
||||
let threshold = threshold.into().threshold();
|
||||
self.threshold = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
||||
|
||||
self.render_state
|
||||
.queue
|
||||
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
|
||||
.submit(std::iter::chain(user_buffers, once(encoder.finish())));
|
||||
|
||||
self.render_state
|
||||
.device
|
||||
|
||||
@@ -43,7 +43,7 @@ fn button_node() {
|
||||
let button_text = "This is a test button!";
|
||||
|
||||
let output = accesskit_output_single_egui_frame(|ui| {
|
||||
CentralPanel::default().show_inside(ui, |ui| ui.button(button_text));
|
||||
CentralPanel::default().show(ui, |ui| ui.button(button_text));
|
||||
});
|
||||
|
||||
let (_, button) = output
|
||||
@@ -61,7 +61,7 @@ fn disabled_button_node() {
|
||||
let button_text = "This is a test button!";
|
||||
|
||||
let output = accesskit_output_single_egui_frame(|ui| {
|
||||
CentralPanel::default().show_inside(ui, |ui| {
|
||||
CentralPanel::default().show(ui, |ui| {
|
||||
ui.add_enabled(false, egui::Button::new(button_text))
|
||||
});
|
||||
});
|
||||
@@ -82,7 +82,7 @@ fn toggle_button_node() {
|
||||
|
||||
let mut selected = false;
|
||||
let output = accesskit_output_single_egui_frame(|ui| {
|
||||
CentralPanel::default().show_inside(ui, |ui| ui.toggle_value(&mut selected, button_text));
|
||||
CentralPanel::default().show(ui, |ui| ui.toggle_value(&mut selected, button_text));
|
||||
});
|
||||
|
||||
let (_, toggle) = output
|
||||
@@ -98,7 +98,7 @@ fn toggle_button_node() {
|
||||
#[test]
|
||||
fn multiple_disabled_widgets() {
|
||||
let output = accesskit_output_single_egui_frame(|ui| {
|
||||
CentralPanel::default().show_inside(ui, |ui| {
|
||||
CentralPanel::default().show(ui, |ui| {
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
let _ = ui.button("Button 1");
|
||||
let _ = ui.button("Button 2");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::containers::menu::{MenuBar, MenuConfig, SubMenuButton};
|
||||
use egui::{PopupCloseBehavior, Ui, include_image};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
use egui_kittest::Harness;
|
||||
use kittest::Queryable as _;
|
||||
|
||||
struct TestMenu {
|
||||
@@ -160,11 +160,12 @@ fn clicking_submenu_button_should_never_close_menu() {
|
||||
assert!(harness.query_by_label("Button in Submenu B").is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "snapshot")]
|
||||
#[test]
|
||||
fn menu_snapshots() {
|
||||
let mut harness = TestMenu::new(MenuConfig::new()).into_harness();
|
||||
|
||||
let mut results = SnapshotResults::new();
|
||||
let mut results = egui_kittest::SnapshotResults::new();
|
||||
|
||||
harness.get_by_label("Menu A").hover();
|
||||
harness.run();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use egui::accesskit::{self, Role};
|
||||
use egui::{Button, ComboBox, Image, Modifiers, Popup, Vec2, Widget as _};
|
||||
use egui::{
|
||||
Align2, Button, ComboBox, FontId, Image, Label, Modifiers, Popup, Pos2, Rect, Stroke,
|
||||
StrokeKind, Vec2, Widget as _, Window,
|
||||
};
|
||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||
use egui_kittest::SnapshotResults;
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
@@ -268,7 +271,7 @@ fn keyboard_submenu_harness() -> Harness<'static, bool> {
|
||||
.with_size(Vec2::new(400.0, 240.0))
|
||||
.build_ui_state(
|
||||
|ui, checked| {
|
||||
egui::Panel::top("menu_bar").show_inside(ui, |ui| {
|
||||
egui::Panel::top("menu_bar").show(ui, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ui.menu_button("X", |ui| {
|
||||
ui.menu_button("Y", |ui| {
|
||||
@@ -337,6 +340,100 @@ pub fn keyboard_should_close_nested_submenu_with_second_enter() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for a bug in `horizontal_wrapped` layouts where text wraps but does not
|
||||
/// move to the next line, causing overlapping text.
|
||||
///
|
||||
/// Sweeps the available width from 200 down to 50 (one frame per width) and asserts that no
|
||||
/// two `TextRun` accesskit nodes (one per laid-out row) have overlapping bounds, and that
|
||||
/// all accesskit text runs and painted text shapes stay within the `horizontal_wrapped` rect.
|
||||
#[test]
|
||||
pub fn horizontal_wrapped_text_should_not_overlap() {
|
||||
struct State {
|
||||
width: f32,
|
||||
rect: egui::Rect,
|
||||
}
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(300.0, 400.0))
|
||||
.build_ui_state(
|
||||
|ui, state: &mut State| {
|
||||
ui.set_width(state.width);
|
||||
state.rect = egui::Frame::popup(ui.style())
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
for i in 0..20 {
|
||||
ui.label(format!("Hello{i}"));
|
||||
}
|
||||
})
|
||||
.response
|
||||
.rect
|
||||
})
|
||||
.inner;
|
||||
},
|
||||
State {
|
||||
width: 200.0,
|
||||
rect: Rect::NAN,
|
||||
},
|
||||
);
|
||||
|
||||
let min_width = 50.0;
|
||||
|
||||
loop {
|
||||
let width = harness.state().width - 1.0;
|
||||
if width < min_width {
|
||||
break;
|
||||
}
|
||||
harness.state_mut().width = width;
|
||||
harness.step();
|
||||
|
||||
let container_rect = harness.state().rect.expand(1.0);
|
||||
|
||||
let runs: Vec<_> = harness
|
||||
.query_all_by_role(accesskit::Role::TextRun)
|
||||
.map(|node| (node.rect(), node.value().unwrap_or_default()))
|
||||
.collect();
|
||||
|
||||
for (rect, text) in &runs {
|
||||
assert!(
|
||||
container_rect.contains_rect(*rect),
|
||||
"TextRun rect at available width = {width} is outside horizontal_wrapped rect: \
|
||||
{text:?} {rect:?} outside {container_rect:?}"
|
||||
);
|
||||
}
|
||||
|
||||
for clipped in &harness.output().shapes {
|
||||
if let egui::epaint::Shape::Text(text_shape) = &clipped.shape {
|
||||
let shape_rect = text_shape.visual_bounding_rect();
|
||||
assert!(
|
||||
container_rect.contains_rect(shape_rect),
|
||||
"TextShape rect at available width = {width} is outside horizontal_wrapped rect: \
|
||||
{:?} {shape_rect:?} outside {container_rect:?}",
|
||||
text_shape.galley.text()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..runs.len() {
|
||||
for j in (i + 1)..runs.len() {
|
||||
let (a, ta) = &runs[i];
|
||||
let (b, tb) = &runs[j];
|
||||
let inter = a.intersect(*b);
|
||||
// Allow tiny floating-point slop for rects that just touch.
|
||||
let overlaps = inter.width() > 0.5 && inter.height() > 0.5;
|
||||
assert!(
|
||||
!overlaps,
|
||||
"TextRun rects overlap at available width = {width}: \
|
||||
{ta:?} {a:?} vs {tb:?} {b:?} \
|
||||
(overlap = {}x{})",
|
||||
inter.width(),
|
||||
inter.height()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
|
||||
let mut harness = keyboard_submenu_harness();
|
||||
@@ -360,3 +457,259 @@ pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
|
||||
"Expected submenu to remain open on repeated pointer click"
|
||||
);
|
||||
}
|
||||
|
||||
/// This test checks if we correctly handle wrapping content proceeding non-wrapping content
|
||||
/// during window resize. When the window is resized past non-wrapping content, the wrapping content
|
||||
/// above should stay at that non wrapping width and not wrap any further.
|
||||
#[test]
|
||||
fn window_resize_wraps_to_content_min_width() {
|
||||
let wrap_text = "This label should wrap as the window is narrowed. \
|
||||
It should not shrink smaller than the bottom labels width though.";
|
||||
let non_wrap_text = "This is the bottom non-wrapping label which is wider.";
|
||||
|
||||
let window_title = "resize_wrap_regression";
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(800.0, 600.0))
|
||||
.build_ui(move |ui| {
|
||||
Window::new(window_title)
|
||||
.default_pos([20.0, 20.0])
|
||||
.default_size([400.0, 200.0])
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.add(Label::new(wrap_text).wrap());
|
||||
ui.add(Label::new(non_wrap_text).extend());
|
||||
});
|
||||
});
|
||||
|
||||
harness.run();
|
||||
|
||||
let window_rect = harness
|
||||
.get_by_role_and_label(Role::Window, window_title)
|
||||
.rect();
|
||||
|
||||
// Drag the right edge inward, well past the non-wrapping label's natural
|
||||
// width, so the non-wrapping label pins the window's minimum width while
|
||||
// the wrapping label would (without the fix) keep shrinking.
|
||||
let grab = Pos2::new(window_rect.right(), window_rect.center().y);
|
||||
let target = Pos2::new(window_rect.left() + 80.0, window_rect.center().y);
|
||||
|
||||
harness.drag_at(grab);
|
||||
harness.run();
|
||||
harness.hover_at(target);
|
||||
|
||||
harness.run();
|
||||
|
||||
let wrap_width = harness.get_by_label(wrap_text).rect().width();
|
||||
let non_wrap_width = harness.get_by_label(non_wrap_text).rect().width();
|
||||
|
||||
// Wrapped text won't perfectly fill the available width — each line ends
|
||||
// wherever the next word stops fitting. The tolerance absorbs that
|
||||
// word-break slack while still catching the bug, where the wrap label
|
||||
// would be substantially narrower than the non-wrapping label.
|
||||
assert!(
|
||||
non_wrap_width - wrap_width < 40.0,
|
||||
"wrapping label width ({wrap_width}) is much narrower than the \
|
||||
non-wrapping label width ({non_wrap_width}) after shrinking the \
|
||||
window past the non-wrapping label's natural width"
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensure that the size passed to window is actually treated as outer size (including
|
||||
/// margins and borders).
|
||||
#[test]
|
||||
fn window_fixed_size_is_outer_size() {
|
||||
use egui::{Color32, Frame, Margin, Pos2, Shape};
|
||||
|
||||
let outer_pos = Pos2::new(50.0, 50.0);
|
||||
let outer_size = Vec2::new(300.0, 200.0);
|
||||
let outer_margin = Margin::same(10);
|
||||
let expected_rect = Rect::from_min_size(outer_pos, outer_size);
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(800.0, 600.0))
|
||||
.build_ui(move |ui| {
|
||||
let frame = Frame::window(ui.style()).outer_margin(outer_margin);
|
||||
Window::new("size_test")
|
||||
.frame(frame)
|
||||
.fixed_pos(outer_pos)
|
||||
.fixed_size(outer_size)
|
||||
.show(ui.ctx(), |ui| {
|
||||
// Fill the available space so `Resize` doesn't auto-shrink the window
|
||||
// below the requested fixed size.
|
||||
ui.allocate_space(ui.available_size());
|
||||
});
|
||||
|
||||
// Paint a debug rect on top of everything that marks the expected outer
|
||||
// window rect. In the snapshot this should line up exactly with the
|
||||
// painted window frame.
|
||||
let painter = ui.ctx().debug_painter();
|
||||
painter.rect_stroke(
|
||||
expected_rect,
|
||||
0.0,
|
||||
Stroke::new(2.0, Color32::RED),
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
painter.text(
|
||||
expected_rect.left_top() + Vec2::new(0.0, -4.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
"should perfectly match the outer window size/position",
|
||||
FontId::default(),
|
||||
Color32::RED,
|
||||
);
|
||||
|
||||
// Also paint the expected *visible frame* rect (outer rect shrunk by the
|
||||
// frame's outer_margin). In the snapshot this should line up exactly with
|
||||
// the painted window frame.
|
||||
let expected_frame_rect = expected_rect - outer_margin;
|
||||
painter.debug_rect(
|
||||
expected_frame_rect,
|
||||
Color32::GREEN,
|
||||
"should perfectly match the painted window frame",
|
||||
);
|
||||
});
|
||||
|
||||
harness.run();
|
||||
|
||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||
harness.snapshot("window_outer_size");
|
||||
|
||||
fn collect_filled_rect_sizes(shape: &Shape, out: &mut Vec<Vec2>) {
|
||||
match shape {
|
||||
// Skip stroke-only rects (fill == TRANSPARENT), so the debug overlay
|
||||
// doesn't trivially satisfy the size check.
|
||||
Shape::Rect(r) if r.fill != Color32::TRANSPARENT => out.push(r.rect.size()),
|
||||
Shape::Vec(v) => v.iter().for_each(|s| collect_filled_rect_sizes(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sizes = Vec::new();
|
||||
for clipped in &harness.output().shapes {
|
||||
collect_filled_rect_sizes(&clipped.shape, &mut sizes);
|
||||
}
|
||||
|
||||
// The shape will have the inner size
|
||||
let painted_size = outer_size - outer_margin.sum();
|
||||
let found = sizes
|
||||
.iter()
|
||||
.any(|s| (s.x - painted_size.x).abs() < 0.5 && (s.y - painted_size.y).abs() < 0.5);
|
||||
|
||||
assert!(
|
||||
found,
|
||||
"expected a filled RectShape with size {painted_size:?} (outer size {outer_size:?} \
|
||||
minus outer margin {outer_margin:?}) in the paint output, but no painted rect matched. \
|
||||
Found filled-rect sizes: {sizes:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/emilk/egui/issues/8055>:
|
||||
/// when content overflows a `Panel`, the returned response (and the panel's
|
||||
/// stored size, resize handle, and separator) must stay clamped to the panel's
|
||||
/// allowed size — they used to inherit the overflowing content rect.
|
||||
#[test]
|
||||
fn panel_rect_clamped_when_content_overflows() {
|
||||
use std::cell::RefCell;
|
||||
|
||||
let side_panel_width = 100.0_f32;
|
||||
let top_panel_height = 80.0_f32;
|
||||
|
||||
let side_response: RefCell<Option<egui::Response>> = RefCell::new(None);
|
||||
let top_response: RefCell<Option<egui::Response>> = RefCell::new(None);
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(400.0, 300.0))
|
||||
.build_ui(|ui| {
|
||||
let r = egui::Panel::left("left_panel")
|
||||
.exact_size(side_panel_width)
|
||||
.show(ui, |ui| {
|
||||
// Allocate way more than the panel — would overflow without the clamp.
|
||||
ui.allocate_space(Vec2::new(1000.0, 10.0));
|
||||
});
|
||||
*side_response.borrow_mut() = Some(r.response);
|
||||
|
||||
let r = egui::Panel::top("top_panel")
|
||||
.exact_size(top_panel_height)
|
||||
.show(ui, |ui| {
|
||||
ui.allocate_space(Vec2::new(10.0, 1000.0));
|
||||
});
|
||||
*top_response.borrow_mut() = Some(r.response);
|
||||
});
|
||||
|
||||
harness.run();
|
||||
|
||||
let sr = side_response.borrow();
|
||||
let sr = sr.as_ref().expect("left panel response was captured");
|
||||
assert!(
|
||||
sr.rect.width() <= side_panel_width + 1.0,
|
||||
"left panel rect.width()={} exceeded the configured panel width {side_panel_width}",
|
||||
sr.rect.width()
|
||||
);
|
||||
assert!(
|
||||
sr.interact_rect.width() <= side_panel_width + 1.0,
|
||||
"left panel interact_rect.width()={} exceeded the configured panel width {side_panel_width}",
|
||||
sr.interact_rect.width()
|
||||
);
|
||||
|
||||
let tr = top_response.borrow();
|
||||
let tr = tr.as_ref().expect("top panel response was captured");
|
||||
assert!(
|
||||
tr.rect.height() <= top_panel_height + 1.0,
|
||||
"top panel rect.height()={} exceeded the configured panel height {top_panel_height}",
|
||||
tr.rect.height()
|
||||
);
|
||||
assert!(
|
||||
tr.interact_rect.height() <= top_panel_height + 1.0,
|
||||
"top panel interact_rect.height()={} exceeded the configured panel height {top_panel_height}",
|
||||
tr.interact_rect.height()
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test: when an animated panel slides off-screen (collapsing), the
|
||||
/// enclosing parent (e.g. a `Window`) must not be grown to include the slid-off
|
||||
/// portion of the panel.
|
||||
#[test]
|
||||
fn collapsing_panel_must_not_grow_enclosing_window() {
|
||||
use std::cell::RefCell;
|
||||
|
||||
let window_rect: RefCell<Option<Rect>> = RefCell::new(None);
|
||||
let is_expanded: RefCell<bool> = RefCell::new(true);
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(800.0, 600.0))
|
||||
.build_ui(|ui| {
|
||||
let resp = egui::Window::new("panels_window")
|
||||
.vscroll(false)
|
||||
.show(ui.ctx(), |ui| {
|
||||
egui::Panel::bottom("bottom_panel")
|
||||
.resizable(false)
|
||||
.min_size(60.0)
|
||||
.show_collapsible(ui, &mut is_expanded.borrow_mut(), |ui| {
|
||||
ui.label("bottom content");
|
||||
});
|
||||
egui::CentralPanel::default().show(ui, |ui| {
|
||||
ui.label("central");
|
||||
});
|
||||
});
|
||||
if let Some(resp) = resp {
|
||||
*window_rect.borrow_mut() = Some(resp.response.rect);
|
||||
}
|
||||
});
|
||||
|
||||
harness.run();
|
||||
let initial = window_rect.borrow().expect("window rect captured");
|
||||
|
||||
// Trigger the collapse animation.
|
||||
*is_expanded.borrow_mut() = false;
|
||||
|
||||
// Step through the animation frames; the window must never grow taller than
|
||||
// its initial height (slid-off panel portion must not push the window out).
|
||||
for i in 0..30 {
|
||||
harness.step();
|
||||
let r = window_rect.borrow().expect("window rect captured");
|
||||
assert!(
|
||||
r.height() <= initial.height() + 0.5,
|
||||
"frame {i}: window grew during panel collapse: initial h={}, now h={}",
|
||||
initial.height(),
|
||||
r.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59939d3347679ff27c1de500a559cdc0e6387f633c86ec7ce51f2a36cd92547b
|
||||
size 7511
|
||||
oid sha256:0267c21c4cbe5601263d046a56596275272fbcf727653131f7780aa51ecf6253
|
||||
size 6956
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df2578c198b29950254ec62c6cc615a4b1c003e7ae3ea027da22fc868b392c74
|
||||
size 8342
|
||||
oid sha256:46a498433ede5b71abccb5bbeed5788e4ba80949b96f3371b107bd45d5dee28a
|
||||
size 7964
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cba6fbd64df18b2a41635af59c1f50d1352b4de8ddefd7d5389a4f9e518b6c86
|
||||
size 23543
|
||||
@@ -1,3 +1,6 @@
|
||||
#![cfg(feature = "snapshot")]
|
||||
#![cfg(feature = "wgpu")]
|
||||
|
||||
use egui::{Modifiers, ScrollArea, Vec2, include_image};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
use kittest::Queryable as _;
|
||||
@@ -122,6 +125,7 @@ fn test_scroll_harness() -> Harness<'static, bool> {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "snapshot")]
|
||||
#[test]
|
||||
fn test_scroll_to_me() {
|
||||
let mut harness = test_scroll_harness();
|
||||
|
||||
Reference in New Issue
Block a user