mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
Panel: never overflow available width, nor max_width (#8198)
* 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.
This commit is contained in:
@@ -18,8 +18,8 @@
|
|||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect, Sense,
|
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
|
||||||
Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
||||||
@@ -618,7 +618,7 @@ impl Panel {
|
|||||||
/// when present, dragging the resize handle past the minimum size collapses
|
/// when present, dragging the resize handle past the minimum size collapses
|
||||||
/// the panel by setting `*is_expanded = false`.
|
/// the panel by setting `*is_expanded = false`.
|
||||||
fn show_inside_dyn<'c, R>(
|
fn show_inside_dyn<'c, R>(
|
||||||
self,
|
mut self,
|
||||||
parent_ui: &mut Ui,
|
parent_ui: &mut Ui,
|
||||||
mut is_expanded: Option<&mut bool>,
|
mut is_expanded: Option<&mut bool>,
|
||||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||||
@@ -627,12 +627,39 @@ impl Panel {
|
|||||||
let id = self.id;
|
let id = self.id;
|
||||||
let resizable = self.resizable;
|
let resizable = self.resizable;
|
||||||
let show_separator_line = self.show_separator_line;
|
let show_separator_line = self.show_separator_line;
|
||||||
let outer_size_range = self.outer_size_range;
|
|
||||||
|
let available_rect = parent_ui.available_rect_before_wrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Never overflow out parent's available width:
|
||||||
|
self.outer_size_range = self.outer_size_range.as_positive();
|
||||||
|
self.outer_size_range.max = f32::min(
|
||||||
|
self.outer_size_range.max,
|
||||||
|
available_rect.size_along(side.axis()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let frame = self.resolve_frame(parent_ui);
|
let frame = self.resolve_frame(parent_ui);
|
||||||
let available_rect = parent_ui.available_rect_before_wrap();
|
|
||||||
let mut outer_size = self.outer_size(parent_ui);
|
// We are NEVER allowed to overflow over this.
|
||||||
let mut outer_rect = self.compute_outer_rect(available_rect, outer_size);
|
// If we do, we do so by clipping the contents,
|
||||||
|
// without reporting that extra size to the parent!
|
||||||
|
let max_rect = {
|
||||||
|
let mut max_rect = available_rect;
|
||||||
|
self.side
|
||||||
|
.set_rect_size(&mut max_rect, self.outer_size_range.max);
|
||||||
|
max_rect
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut outer_size = self
|
||||||
|
.outer_size(parent_ui)
|
||||||
|
.at_most(available_rect.size_along(self.side.axis()));
|
||||||
|
|
||||||
|
let mut outer_rect = {
|
||||||
|
let mut outer_rect = available_rect;
|
||||||
|
self.side.set_rect_size(&mut outer_rect, outer_size);
|
||||||
|
outer_rect
|
||||||
|
};
|
||||||
|
|
||||||
// Check for duplicate id
|
// Check for duplicate id
|
||||||
parent_ui.check_for_id_clash(id, outer_rect, "Panel");
|
parent_ui.check_for_id_clash(id, outer_rect, "Panel");
|
||||||
@@ -671,7 +698,7 @@ impl Panel {
|
|||||||
// `.abs()` here, that would mirror the drag and spuriously
|
// `.abs()` here, that would mirror the drag and spuriously
|
||||||
// trigger drag-to-expand once the pointer crosses the edge.
|
// trigger drag-to-expand once the pointer crosses the edge.
|
||||||
let raw_outer_size = -side.sign() * (pointer[axis] - side.fixed_pos(outer_rect));
|
let raw_outer_size = -side.sign() * (pointer[axis] - side.fixed_pos(outer_rect));
|
||||||
outer_size = clamp_to_range(raw_outer_size, outer_size_range)
|
outer_size = clamp_to_range(raw_outer_size, self.outer_size_range)
|
||||||
.at_most(available_rect.size_along(axis));
|
.at_most(available_rect.size_along(axis));
|
||||||
side.set_rect_size(&mut outer_rect, outer_size);
|
side.set_rect_size(&mut outer_rect, outer_size);
|
||||||
|
|
||||||
@@ -684,7 +711,7 @@ impl Panel {
|
|||||||
// Use `raw_outer_size` (pre-clamp) so a tight `exact_size`
|
// Use `raw_outer_size` (pre-clamp) so a tight `exact_size`
|
||||||
// panel can still detect inward overshoot.
|
// panel can still detect inward overshoot.
|
||||||
let collapse_threshold =
|
let collapse_threshold =
|
||||||
self.collapse_threshold.unwrap_or(outer_size_range.min);
|
self.collapse_threshold.unwrap_or(self.outer_size_range.min);
|
||||||
if raw_outer_size < collapse_threshold && raw_outer_size < prev_outer_size {
|
if raw_outer_size < collapse_threshold && raw_outer_size < prev_outer_size {
|
||||||
*is_expanded = false;
|
*is_expanded = false;
|
||||||
}
|
}
|
||||||
@@ -694,7 +721,7 @@ impl Panel {
|
|||||||
// to the expanded panel's id. `raw_outer_size` is required
|
// to the expanded panel's id. `raw_outer_size` is required
|
||||||
// because `outer_size` is clamped to `max` and would never
|
// because `outer_size` is clamped to `max` and would never
|
||||||
// exceed it (so `exact_size` panels couldn't otherwise expand).
|
// exceed it (so `exact_size` panels couldn't otherwise expand).
|
||||||
if outer_size_range.max < raw_outer_size {
|
if self.outer_size_range.max < raw_outer_size {
|
||||||
*is_expanded = true;
|
*is_expanded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -715,9 +742,10 @@ impl Panel {
|
|||||||
.translate(slide_distance * side.dir_vec2())
|
.translate(slide_distance * side.dir_vec2())
|
||||||
.round_ui()
|
.round_ui()
|
||||||
};
|
};
|
||||||
|
|
||||||
// The portion of the panel actually visible inside the parent's available area.
|
// The portion of the panel actually visible inside the parent's available area.
|
||||||
// The parent only allocates this much; neighbors follow the slide.
|
// The parent only allocates this much; neighbors follow the slide.
|
||||||
let visible_outer_rect = shifted_outer_rect.intersect(available_rect);
|
let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
|
||||||
|
|
||||||
let mut panel_ui = parent_ui.new_child(
|
let mut panel_ui = parent_ui.new_child(
|
||||||
UiBuilder::new()
|
UiBuilder::new()
|
||||||
@@ -731,8 +759,8 @@ impl Panel {
|
|||||||
|
|
||||||
let axis = side.axis();
|
let axis = side.axis();
|
||||||
let panel_axis_min =
|
let panel_axis_min =
|
||||||
(outer_size_range.min - frame.total_margin().sum()[axis]).at_least(0.0);
|
(self.outer_size_range.min - frame.total_margin().sum()[axis]).at_least(0.0);
|
||||||
let inner_response = frame.show(&mut panel_ui, |content_ui| {
|
let mut inner_response = frame.show(&mut panel_ui, |content_ui| {
|
||||||
// Make sure the frame fills the cross-axis fully:
|
// Make sure the frame fills the cross-axis fully:
|
||||||
let cross_axis_size = content_ui.max_rect().size_along(side.cross_axis());
|
let cross_axis_size = content_ui.max_rect().size_along(side.cross_axis());
|
||||||
if axis == 0 {
|
if axis == 0 {
|
||||||
@@ -746,9 +774,14 @@ impl Panel {
|
|||||||
add_contents(content_ui)
|
add_contents(content_ui)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if self.outer_size_range.max < inner_response.response.rect.size_along(axis) {
|
||||||
|
self.side
|
||||||
|
.set_rect_size(&mut inner_response.response.rect, self.outer_size_range.max);
|
||||||
|
}
|
||||||
|
|
||||||
// `Frame::show` returns the panel's (shifted) _outer_ rect, including margin & border.
|
// `Frame::show` returns the panel's (shifted) _outer_ rect, including margin & border.
|
||||||
let shifted_outer_rect = inner_response.response.rect;
|
let shifted_outer_rect = inner_response.response.rect;
|
||||||
let visible_outer_rect = shifted_outer_rect.intersect(available_rect);
|
let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut cursor = parent_ui.cursor();
|
let mut cursor = parent_ui.cursor();
|
||||||
@@ -769,7 +802,8 @@ impl Panel {
|
|||||||
// Now we do the actual resize interaction, on top of all the contents,
|
// Now we do the actual resize interaction, on top of all the contents,
|
||||||
// otherwise its input could be eaten by the contents, e.g. a
|
// otherwise its input could be eaten by the contents, e.g. a
|
||||||
// `ScrollArea` on either side of the panel boundary.
|
// `ScrollArea` on either side of the panel boundary.
|
||||||
self.resize_panel(shifted_outer_rect, parent_ui)
|
let resize_response = self.resize_panel(shifted_outer_rect, parent_ui);
|
||||||
|
(resize_response.hovered(), resize_response.dragged())
|
||||||
} else {
|
} else {
|
||||||
(false, false)
|
(false, false)
|
||||||
};
|
};
|
||||||
@@ -892,16 +926,7 @@ impl Panel {
|
|||||||
clamp_to_range(raw, self.outer_size_range)
|
clamp_to_range(raw, self.outer_size_range)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clamp `outer_size` to the allowed range / available space, then compute the panel rect.
|
fn resize_panel(&self, outer_rect: Rect, ui: &Ui) -> Response {
|
||||||
fn compute_outer_rect(&self, available_rect: Rect, mut outer_size: f32) -> Rect {
|
|
||||||
let mut outer_rect = available_rect;
|
|
||||||
outer_size = clamp_to_range(outer_size, self.outer_size_range)
|
|
||||||
.at_most(available_rect.size_along(self.side.axis()));
|
|
||||||
self.side.set_rect_size(&mut outer_rect, outer_size);
|
|
||||||
outer_rect
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize_panel(&self, outer_rect: Rect, ui: &Ui) -> (bool, bool) {
|
|
||||||
let resize_pos = self.side.resize_pos(outer_rect);
|
let resize_pos = self.side.resize_pos(outer_rect);
|
||||||
let panel_axis_range = Rangef::point(resize_pos);
|
let panel_axis_range = Rangef::point(resize_pos);
|
||||||
let cross_range = outer_rect.range_along(self.side.cross_axis());
|
let cross_range = outer_rect.range_along(self.side.cross_axis());
|
||||||
@@ -916,9 +941,7 @@ impl Panel {
|
|||||||
// `show_switched` share one resize widget.
|
// `show_switched` share one resize widget.
|
||||||
let resize_id = self.resize_id_source.unwrap_or(self.id).with("__resize");
|
let resize_id = self.resize_id_source.unwrap_or(self.id).with("__resize");
|
||||||
let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
|
let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
|
||||||
let resize_response = ui.interact(resize_rect, resize_id, Sense::click_and_drag());
|
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
|
||||||
|
|
||||||
(resize_response.hovered(), resize_response.dragged())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_icon(&self, outer_size: f32) -> CursorIcon {
|
fn cursor_icon(&self, outer_size: f32) -> CursorIcon {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use egui::containers::menu::{MenuBar, MenuConfig, SubMenuButton};
|
use egui::containers::menu::{MenuBar, MenuConfig, SubMenuButton};
|
||||||
use egui::{PopupCloseBehavior, Ui, include_image};
|
use egui::{PopupCloseBehavior, Ui, include_image};
|
||||||
use egui_kittest::{Harness, SnapshotResults};
|
use egui_kittest::Harness;
|
||||||
use kittest::Queryable as _;
|
use kittest::Queryable as _;
|
||||||
|
|
||||||
struct TestMenu {
|
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());
|
assert!(harness.query_by_label("Button in Submenu B").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "snapshot")]
|
||||||
#[test]
|
#[test]
|
||||||
fn menu_snapshots() {
|
fn menu_snapshots() {
|
||||||
let mut harness = TestMenu::new(MenuConfig::new()).into_harness();
|
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.get_by_label("Menu A").hover();
|
||||||
harness.run();
|
harness.run();
|
||||||
|
|||||||
@@ -600,3 +600,116 @@ fn window_fixed_size_is_outer_size() {
|
|||||||
Found filled-rect sizes: {sizes:?}"
|
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,6 @@
|
|||||||
|
#![cfg(feature = "snapshot")]
|
||||||
|
#![cfg(feature = "wgpu")]
|
||||||
|
|
||||||
use egui::{Modifiers, ScrollArea, Vec2, include_image};
|
use egui::{Modifiers, ScrollArea, Vec2, include_image};
|
||||||
use egui_kittest::{Harness, SnapshotResults};
|
use egui_kittest::{Harness, SnapshotResults};
|
||||||
use kittest::Queryable as _;
|
use kittest::Queryable as _;
|
||||||
@@ -122,6 +125,7 @@ fn test_scroll_harness() -> Harness<'static, bool> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "snapshot")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scroll_to_me() {
|
fn test_scroll_to_me() {
|
||||||
let mut harness = test_scroll_harness();
|
let mut harness = test_scroll_harness();
|
||||||
|
|||||||
Reference in New Issue
Block a user