mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 12:50:04 -04:00
* Closes https://github.com/emilk/egui/issues/8055 * Closes https://github.com/emilk/egui/pull/8056 This changes the behavior of `Panel` to NEVER overflow `Panel::max_size`, nor the available space in the parent UI. If you do overflow it, the content will be silently clipped.
716 lines
24 KiB
Rust
716 lines
24 KiB
Rust
use egui::accesskit::{self, Role};
|
|
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 _};
|
|
|
|
#[test]
|
|
pub fn focus_should_skip_over_disabled_buttons() {
|
|
let mut harness = Harness::new_ui(|ui| {
|
|
ui.add(Button::new("Button 1"));
|
|
ui.add_enabled(false, Button::new("Button Disabled"));
|
|
ui.add(Button::new("Button 3"));
|
|
});
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let button_1 = harness.get_by_label("Button 1");
|
|
assert!(button_1.is_focused());
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let button_3 = harness.get_by_label("Button 3");
|
|
assert!(button_3.is_focused());
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let button_1 = harness.get_by_label("Button 1");
|
|
assert!(button_1.is_focused());
|
|
}
|
|
|
|
#[test]
|
|
pub fn focus_should_skip_over_disabled_drag_values() {
|
|
let mut value_1: u16 = 1;
|
|
let mut value_2: u16 = 2;
|
|
let mut value_3: u16 = 3;
|
|
|
|
let mut harness = Harness::new_ui(|ui| {
|
|
ui.add(egui::DragValue::new(&mut value_1));
|
|
ui.add_enabled(false, egui::DragValue::new(&mut value_2));
|
|
ui.add(egui::DragValue::new(&mut value_3));
|
|
});
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let drag_value_1 = harness.get_by(|node| node.numeric_value() == Some(1.0));
|
|
assert!(drag_value_1.is_focused());
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let drag_value_3 = harness.get_by(|node| node.numeric_value() == Some(3.0));
|
|
assert!(drag_value_3.is_focused());
|
|
}
|
|
|
|
#[test]
|
|
fn image_failed() {
|
|
let mut harness = Harness::new_ui(|ui| {
|
|
Image::new("file://invalid/path")
|
|
.alt_text("I have an alt text")
|
|
.max_size(Vec2::new(100.0, 100.0))
|
|
.ui(ui);
|
|
});
|
|
|
|
harness.run();
|
|
harness.fit_contents();
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
harness.snapshot("image_snapshots");
|
|
}
|
|
|
|
#[test]
|
|
fn test_combobox() {
|
|
let items = ["Item 1", "Item 2", "Item 3"];
|
|
let mut harness = Harness::builder()
|
|
.with_size(Vec2::new(300.0, 200.0))
|
|
.build_ui_state(
|
|
|ui, selected| {
|
|
ComboBox::new("combobox", "Select Something").show_index(
|
|
ui,
|
|
selected,
|
|
items.len(),
|
|
|idx| *items.get(idx).expect("Invalid index"),
|
|
);
|
|
},
|
|
0,
|
|
);
|
|
|
|
harness.run();
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
let mut results = SnapshotResults::new();
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
results.add(harness.try_snapshot("combobox_closed"));
|
|
|
|
let combobox = harness.get_by_role_and_label(Role::ComboBox, "Select Something");
|
|
combobox.click();
|
|
|
|
harness.run();
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
results.add(harness.try_snapshot("combobox_opened"));
|
|
|
|
let item_2 = harness.get_by_role_and_label(Role::Button, "Item 2");
|
|
item_2.click();
|
|
|
|
harness.run();
|
|
|
|
assert_eq!(harness.state(), &1);
|
|
|
|
// Popup should be closed now
|
|
assert!(harness.query_by_label("Item 2").is_none());
|
|
}
|
|
|
|
/// `https://github.com/emilk/egui/issues/7065`
|
|
#[test]
|
|
pub fn slider_should_move_with_fixed_decimals() {
|
|
let mut value: f32 = 1.0;
|
|
|
|
let mut harness = Harness::new_ui(|ui| {
|
|
// Movement on arrow-key is relative to slider width; make the slider wide so the movement becomes small.
|
|
ui.spacing_mut().slider_width = 2000.0;
|
|
ui.add(egui::Slider::new(&mut value, 0.1..=10.0).fixed_decimals(2));
|
|
});
|
|
|
|
harness.key_press(egui::Key::Tab);
|
|
harness.run();
|
|
|
|
let actual_slider = harness.get_by_role(accesskit::Role::SpinButton);
|
|
assert_eq!(actual_slider.value(), Some("1.00".to_owned()));
|
|
|
|
harness.key_press(egui::Key::ArrowRight);
|
|
harness.run();
|
|
|
|
let actual_slider = harness.get_by_role(accesskit::Role::SpinButton);
|
|
assert_eq!(actual_slider.value(), Some("1.01".to_owned()));
|
|
|
|
harness.key_press(egui::Key::ArrowRight);
|
|
harness.run();
|
|
|
|
let actual_slider = harness.get_by_role(accesskit::Role::SpinButton);
|
|
assert_eq!(actual_slider.value(), Some("1.02".to_owned()));
|
|
|
|
harness.key_press(egui::Key::ArrowLeft);
|
|
harness.run();
|
|
|
|
let actual_slider = harness.get_by_role(accesskit::Role::SpinButton);
|
|
assert_eq!(actual_slider.value(), Some("1.01".to_owned()));
|
|
|
|
harness.key_press(egui::Key::ArrowLeft);
|
|
harness.run();
|
|
|
|
let actual_slider = harness.get_by_role(accesskit::Role::SpinButton);
|
|
assert_eq!(actual_slider.value(), Some("1.00".to_owned()));
|
|
}
|
|
|
|
#[test]
|
|
pub fn override_text_color_affects_interactive_widgets() {
|
|
use egui::{Color32, RichText};
|
|
|
|
let mut harness = Harness::new_ui(|ui| {
|
|
_ = ui.button("normal");
|
|
_ = ui.checkbox(&mut true, "normal");
|
|
_ = ui.radio(true, "normal");
|
|
ui.visuals_mut().widgets.inactive.fg_stroke.color = Color32::RED;
|
|
_ = ui.button("red");
|
|
_ = ui.checkbox(&mut true, "red");
|
|
_ = ui.radio(true, "red");
|
|
// override_text_color takes precedence over `WidgetVisuals`, as it docstring claims
|
|
ui.visuals_mut().override_text_color = Some(Color32::GREEN);
|
|
_ = ui.button("green");
|
|
_ = ui.checkbox(&mut true, "green");
|
|
_ = ui.radio(true, "green");
|
|
// Setting the color explicitly with `RichText` overrides style
|
|
_ = ui.button(RichText::new("blue").color(Color32::BLUE));
|
|
_ = ui.checkbox(&mut true, RichText::new("blue").color(Color32::BLUE));
|
|
_ = ui.radio(true, RichText::new("blue").color(Color32::BLUE));
|
|
});
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
let mut results = SnapshotResults::new();
|
|
|
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
|
results.add(harness.try_snapshot("override_text_color_interactive"));
|
|
}
|
|
|
|
/// <https://github.com/rerun-io/rerun/issues/11301>
|
|
#[test]
|
|
pub fn menus_should_close_even_if_submenu_disappears() {
|
|
const OTHER_BUTTON: &str = "Other button";
|
|
const MENU_BUTTON: &str = "Menu";
|
|
const SUB_MENU_BUTTON: &str = "Always here";
|
|
const TOGGLEABLE_SUB_MENU_BUTTON: &str = "Maybe here";
|
|
const INSIDE_SUB_MENU_BUTTON: &str = "Inside submenu";
|
|
|
|
for frame_delay in (0..3).rev() {
|
|
let mut harness = Harness::builder().build_ui_state(
|
|
|ui, state| {
|
|
let _ = ui.button(OTHER_BUTTON).clicked();
|
|
let response = ui.button(MENU_BUTTON);
|
|
|
|
Popup::menu(&response).show(|ui| {
|
|
let _ = ui.button(SUB_MENU_BUTTON);
|
|
if *state {
|
|
ui.menu_button(TOGGLEABLE_SUB_MENU_BUTTON, |ui| {
|
|
let _ = ui.button(INSIDE_SUB_MENU_BUTTON);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
true,
|
|
);
|
|
|
|
// Open the main menu
|
|
harness.get_by_label(MENU_BUTTON).click();
|
|
harness.run();
|
|
|
|
// Open the sub menu
|
|
harness
|
|
.get_by_label_contains(TOGGLEABLE_SUB_MENU_BUTTON)
|
|
.hover();
|
|
harness.run();
|
|
|
|
// Have we opened the submenu successfully?
|
|
harness.get_by_label(INSIDE_SUB_MENU_BUTTON).hover();
|
|
harness.run();
|
|
|
|
// We click manually, since we want to precisely time that the sub menu disappears when the
|
|
// button is released
|
|
let center = harness.get_by_label(OTHER_BUTTON).rect().center();
|
|
harness.input_mut().events.push(egui::Event::PointerButton {
|
|
pos: center,
|
|
button: egui::PointerButton::Primary,
|
|
pressed: true,
|
|
modifiers: Modifiers::default(),
|
|
});
|
|
harness.step();
|
|
|
|
// Yank the sub menu from under the pointer
|
|
*harness.state_mut() = false;
|
|
|
|
// See if we handle it with or without a frame delay
|
|
harness.run_steps(frame_delay);
|
|
|
|
// Actually close the menu by clicking somewhere outside
|
|
harness.input_mut().events.push(egui::Event::PointerButton {
|
|
pos: center,
|
|
button: egui::PointerButton::Primary,
|
|
pressed: false,
|
|
modifiers: Modifiers::default(),
|
|
});
|
|
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label_contains(SUB_MENU_BUTTON).is_none(),
|
|
"Menu failed to close. frame_delay = {frame_delay}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn keyboard_submenu_harness() -> Harness<'static, bool> {
|
|
Harness::builder()
|
|
.with_size(Vec2::new(400.0, 240.0))
|
|
.build_ui_state(
|
|
|ui, checked| {
|
|
egui::Panel::top("menu_bar").show(ui, |ui| {
|
|
egui::MenuBar::new().ui(ui, |ui| {
|
|
ui.menu_button("X", |ui| {
|
|
ui.menu_button("Y", |ui| {
|
|
ui.checkbox(checked, "Goal");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
pub fn keyboard_should_open_nested_submenu() {
|
|
let mut harness = keyboard_submenu_harness();
|
|
|
|
harness.get_by_label("X").focus();
|
|
harness.run();
|
|
|
|
harness.key_press(egui::Key::Enter);
|
|
harness.run();
|
|
|
|
harness.get_by_label_contains("Y").focus();
|
|
harness.run();
|
|
|
|
harness.key_press(egui::Key::Enter);
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label("Goal").is_some(),
|
|
"Expected nested submenu to open via keyboard"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn keyboard_should_close_nested_submenu_with_second_enter() {
|
|
let mut harness = keyboard_submenu_harness();
|
|
|
|
harness.get_by_label("X").focus();
|
|
harness.run();
|
|
|
|
harness.key_press(egui::Key::Enter);
|
|
harness.run();
|
|
|
|
harness.get_by_label_contains("Y").focus();
|
|
harness.run();
|
|
|
|
harness.key_press(egui::Key::Enter);
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label("Goal").is_some(),
|
|
"Expected nested submenu to open before close attempt"
|
|
);
|
|
|
|
harness.get_by_label_contains("Y").focus();
|
|
harness.run();
|
|
|
|
harness.key_press(egui::Key::Enter);
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label("Goal").is_none(),
|
|
"Expected nested submenu to close when pressing Enter again"
|
|
);
|
|
}
|
|
|
|
/// 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();
|
|
|
|
harness.get_by_label("X").click();
|
|
harness.run();
|
|
|
|
harness.get_by_label_contains("Y").click();
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label("Goal").is_some(),
|
|
"Expected submenu to remain open after pointer click on its button"
|
|
);
|
|
|
|
harness.get_by_label_contains("Y").click();
|
|
harness.run();
|
|
|
|
assert!(
|
|
harness.query_by_label("Goal").is_some(),
|
|
"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(),
|
|
);
|
|
}
|
|
}
|