1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 22:00:03 -04:00

Refactor Panels (#8174)

In preparation for nicer panel animation.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Emil Ernerfeldt
2026-05-19 14:41:16 +02:00
committed by GitHub
parent 7dba2e99fa
commit bcfb5bf493
3 changed files with 267 additions and 426 deletions

View File

@@ -398,11 +398,15 @@ impl Frame {
} }
/// Show the given ui surrounded by this frame. /// Show the given ui surrounded by this frame.
///
/// The returned [`InnerResponse::response`] will have the rect of the entire frame, including margins.
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> { pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.show_dyn(ui, Box::new(add_contents)) self.show_dyn(ui, Box::new(add_contents))
} }
/// Show using dynamic dispatch. /// Show using dynamic dispatch.
///
/// The returned [`InnerResponse::response`] will have the rect of the entire frame, including margins.
pub fn show_dyn<'c, R>( pub fn show_dyn<'c, R>(
self, self,
ui: &mut Ui, ui: &mut Ui,

View File

@@ -15,11 +15,11 @@
//! //!
//! Add your [`crate::Window`]:s after any top-level panels. //! Add your [`crate::Window`]:s after any top-level panels.
use emath::{GuiRounding as _, Pos2}; 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, Sense,
Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2, 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 {
@@ -30,7 +30,9 @@ fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PanelState { pub struct PanelState {
pub rect: Rect, /// The _outer_ rect of the panel, i.e. including the [`Frame`] margin & border.
#[cfg_attr(feature = "serde", serde(alias = "rect"))]
pub outer_rect: Rect,
} }
impl PanelState { impl PanelState {
@@ -38,9 +40,10 @@ impl PanelState {
ctx.data_mut(|d| d.get_persisted(bar_id)) ctx.data_mut(|d| d.get_persisted(bar_id))
} }
/// The size of the panel (from previous frame). /// The _outer_ size of the panel (from previous frame),
/// i.e. including the [`Frame`] margin & border.
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.rect.size() self.outer_rect.size()
} }
fn store(self, ctx: &Context, bar_id: Id) { fn store(self, ctx: &Context, bar_id: Id) {
@@ -50,225 +53,85 @@ impl PanelState {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// [`Left`](VerticalSide::Left) or [`Right`](VerticalSide::Right) /// Which side of a [`Ui`] or screen the panel is attached to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum VerticalSide { enum PanelSide {
Left, Left,
Right, Right,
}
impl VerticalSide {
pub fn opposite(self) -> Self {
match self {
Self::Left => Self::Right,
Self::Right => Self::Left,
}
}
/// `self` is the _fixed_ side.
///
/// * Left panels are resized on their right side
/// * Right panels are resized on their left side
fn set_rect_width(self, rect: &mut Rect, width: f32) {
match self {
Self::Left => rect.max.x = rect.min.x + width,
Self::Right => rect.min.x = rect.max.x - width,
}
}
fn sign(self) -> f32 {
match self {
Self::Left => -1.0,
Self::Right => 1.0,
}
}
fn side_x(self, rect: Rect) -> f32 {
match self {
Self::Left => rect.left(),
Self::Right => rect.right(),
}
}
}
/// [`Top`](HorizontalSide::Top) or [`Bottom`](HorizontalSide::Bottom)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HorizontalSide {
Top, Top,
Bottom, Bottom,
} }
impl HorizontalSide { impl PanelSide {
pub fn opposite(self) -> Self { /// The axis the panel grows along: `0` (x) for left/right panels,
match self { /// `1` (y) for top/bottom panels.
Self::Top => Self::Bottom,
Self::Bottom => Self::Top,
}
}
/// `self` is the _fixed_ side.
/// ///
/// * Top panels are resized on their bottom side /// Useful as an index into `Vec2`/`Pos2`.
/// * Bottom panels are resized upwards fn axis(self) -> usize {
fn set_rect_height(self, rect: &mut Rect, height: f32) {
match self { match self {
Self::Top => rect.max.y = rect.min.y + height, Self::Left | Self::Right => 0,
Self::Bottom => rect.min.y = rect.max.y - height, Self::Top | Self::Bottom => 1,
} }
} }
/// The axis perpendicular to [`Self::axis`].
fn cross_axis(self) -> usize {
1 - self.axis()
}
/// Unit vector along [`Self::axis`]: `(1, 0)` for left/right, `(0, 1)` for top/bottom.
fn axis_unit(self) -> Vec2 {
match self {
Self::Left | Self::Right => Vec2::X,
Self::Top | Self::Bottom => Vec2::Y,
}
}
/// `-1` for sides at the near edge ([`Left`](Self::Left), [`Top`](Self::Top)),
/// `+1` for sides at the far edge ([`Right`](Self::Right), [`Bottom`](Self::Bottom)).
fn sign(self) -> f32 { fn sign(self) -> f32 {
match self { match self {
Self::Top => -1.0, Self::Left | Self::Top => -1.0,
Self::Bottom => 1.0, Self::Right | Self::Bottom => 1.0,
} }
} }
fn side_y(self, rect: Rect) -> f32 { /// Coordinate of the _fixed_ side along the panel's [`axis`](Self::axis).
fn fixed_pos(self, rect: Rect) -> f32 {
match self { match self {
Self::Left => rect.left(),
Self::Right => rect.right(),
Self::Top => rect.top(), Self::Top => rect.top(),
Self::Bottom => rect.bottom(), Self::Bottom => rect.bottom(),
} }
} }
}
// Intentionally private because I'm not sure of the naming. /// Coordinate of the _opposite_ (resizable) side along the panel's [`axis`](Self::axis).
// TODO(emilk): decide on good names and make public. fn resize_pos(self, rect: Rect) -> f32 {
// "VerticalSide" and "HorizontalSide" feels inverted to me. match self {
/// [`Horizontal`](PanelSide::Horizontal) or [`Vertical`](PanelSide::Vertical) Self::Left => rect.right(),
#[derive(Clone, Copy, Debug, PartialEq, Eq)] Self::Right => rect.left(),
enum PanelSide { Self::Top => rect.bottom(),
/// Left or right. Self::Bottom => rect.top(),
Vertical(VerticalSide), }
/// Top or bottom
Horizontal(HorizontalSide),
}
impl From<HorizontalSide> for PanelSide {
fn from(side: HorizontalSide) -> Self {
Self::Horizontal(side)
} }
}
impl From<VerticalSide> for PanelSide { /// Resize by keeping `self` side fixed, and moving the opposite side.
fn from(side: VerticalSide) -> Self {
Self::Vertical(side)
}
}
impl PanelSide {
pub const LEFT: Self = Self::Vertical(VerticalSide::Left);
pub const RIGHT: Self = Self::Vertical(VerticalSide::Right);
pub const TOP: Self = Self::Horizontal(HorizontalSide::Top);
pub const BOTTOM: Self = Self::Horizontal(HorizontalSide::Bottom);
/// Resize by keeping the [`self`] side fixed, and moving the opposite side.
fn set_rect_size(self, rect: &mut Rect, size: f32) { fn set_rect_size(self, rect: &mut Rect, size: f32) {
match self { match self {
Self::Vertical(side) => side.set_rect_width(rect, size), Self::Left => rect.max.x = rect.min.x + size,
Self::Horizontal(side) => side.set_rect_height(rect, size), Self::Right => rect.min.x = rect.max.x - size,
Self::Top => rect.max.y = rect.min.y + size,
Self::Bottom => rect.min.y = rect.max.y - size,
} }
} }
fn ui_kind(self) -> UiKind { fn ui_kind(self) -> UiKind {
match self { match self {
Self::Vertical(side) => match side { Self::Left => UiKind::LeftPanel,
VerticalSide::Left => UiKind::LeftPanel, Self::Right => UiKind::RightPanel,
VerticalSide::Right => UiKind::RightPanel, Self::Top => UiKind::TopPanel,
}, Self::Bottom => UiKind::BottomPanel,
Self::Horizontal(side) => match side {
HorizontalSide::Top => UiKind::TopPanel,
HorizontalSide::Bottom => UiKind::BottomPanel,
},
}
}
}
// ----------------------------------------------------------------------------
/// Intermediate structure to abstract some portion of [`Panel::show_inside`](Panel::show_inside).
struct PanelSizer<'a> {
panel: &'a Panel,
frame: Frame,
available_rect: Rect,
size: f32,
panel_rect: Rect,
}
impl<'a> PanelSizer<'a> {
fn new(panel: &'a Panel, ui: &Ui) -> Self {
let frame = panel
.frame
.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let available_rect = ui.available_rect_before_wrap();
let size = PanelSizer::get_size_from_state_or_default(panel, ui, frame);
let panel_rect = PanelSizer::panel_rect(panel, available_rect, size);
Self {
panel,
frame,
available_rect,
size,
panel_rect,
}
}
fn get_size_from_state_or_default(panel: &Panel, ui: &Ui, frame: Frame) -> f32 {
if let Some(state) = PanelState::load(ui.ctx(), panel.id) {
match panel.side {
PanelSide::Vertical(_) => state.rect.width(),
PanelSide::Horizontal(_) => state.rect.height(),
}
} else {
match panel.side {
PanelSide::Vertical(_) => panel.default_size.unwrap_or_else(|| {
ui.style().spacing.interact_size.x + frame.inner_margin.sum().x
}),
PanelSide::Horizontal(_) => panel.default_size.unwrap_or_else(|| {
ui.style().spacing.interact_size.y + frame.inner_margin.sum().y
}),
}
}
}
fn panel_rect(panel: &Panel, available_rect: Rect, mut size: f32) -> Rect {
let side = panel.side;
let size_range = panel.size_range;
let mut panel_rect = available_rect;
match side {
PanelSide::Vertical(_) => {
size = clamp_to_range(size, size_range).at_most(available_rect.width());
}
PanelSide::Horizontal(_) => {
size = clamp_to_range(size, size_range).at_most(available_rect.height());
}
}
side.set_rect_size(&mut panel_rect, size);
panel_rect
}
fn prepare_resizing_response(&mut self, is_resizing: bool, pointer: Option<Pos2>) {
let side = self.panel.side;
let size_range = self.panel.size_range;
if is_resizing && let Some(pointer) = pointer {
match side {
PanelSide::Vertical(side) => {
self.size = (pointer.x - side.side_x(self.panel_rect)).abs();
self.size =
clamp_to_range(self.size, size_range).at_most(self.available_rect.width());
}
PanelSide::Horizontal(side) => {
self.size = (pointer.y - side.side_y(self.panel_rect)).abs();
self.size =
clamp_to_range(self.size, size_range).at_most(self.available_rect.height());
}
}
side.set_rect_size(&mut self.panel_rect, self.size);
} }
} }
} }
@@ -302,13 +165,13 @@ pub struct Panel {
resizable: bool, resizable: bool,
show_separator_line: bool, show_separator_line: bool,
/// The size is defined as being either the width for a Vertical Panel /// _Outer_ size (including [`Frame`] margin & border):
/// or the height for a Horizontal Panel. /// the width for a vertical panel, or the height for a horizontal panel.
default_size: Option<f32>, default_outer_size: Option<f32>,
/// The size is defined as being either the width for a Vertical Panel /// _Outer_ size range (including [`Frame`] margin & border):
/// or the height for a Horizontal Panel. /// the width for a vertical panel, or the height for a horizontal panel.
size_range: Rangef, outer_size_range: Rangef,
} }
impl Panel { impl Panel {
@@ -316,14 +179,14 @@ impl Panel {
/// ///
/// The id should be globally unique, e.g. `Id::new("my_left_panel")`. /// The id should be globally unique, e.g. `Id::new("my_left_panel")`.
pub fn left(id: impl Into<Id>) -> Self { pub fn left(id: impl Into<Id>) -> Self {
Self::new(PanelSide::LEFT, id) Self::new(PanelSide::Left, id)
} }
/// Create a right panel. /// Create a right panel.
/// ///
/// The id should be globally unique, e.g. `Id::new("my_right_panel")`. /// The id should be globally unique, e.g. `Id::new("my_right_panel")`.
pub fn right(id: impl Into<Id>) -> Self { pub fn right(id: impl Into<Id>) -> Self {
Self::new(PanelSide::RIGHT, id) Self::new(PanelSide::Right, id)
} }
/// Create a top panel. /// Create a top panel.
@@ -332,7 +195,7 @@ impl Panel {
/// ///
/// By default this is NOT resizable. /// By default this is NOT resizable.
pub fn top(id: impl Into<Id>) -> Self { pub fn top(id: impl Into<Id>) -> Self {
Self::new(PanelSide::TOP, id).resizable(false) Self::new(PanelSide::Top, id).resizable(false)
} }
/// Create a bottom panel. /// Create a bottom panel.
@@ -341,21 +204,21 @@ impl Panel {
/// ///
/// By default this is NOT resizable. /// By default this is NOT resizable.
pub fn bottom(id: impl Into<Id>) -> Self { pub fn bottom(id: impl Into<Id>) -> Self {
Self::new(PanelSide::BOTTOM, id).resizable(false) Self::new(PanelSide::Bottom, id).resizable(false)
} }
/// Create a panel. /// Create a panel.
/// ///
/// The id should be globally unique, e.g. `Id::new("my_panel")`. /// The id should be globally unique, e.g. `Id::new("my_panel")`.
fn new(side: PanelSide, id: impl Into<Id>) -> Self { fn new(side: PanelSide, id: impl Into<Id>) -> Self {
let default_size: Option<f32> = match side { let default_outer_size: Option<f32> = match side {
PanelSide::Vertical(_) => Some(200.0), PanelSide::Left | PanelSide::Right => Some(200.0),
PanelSide::Horizontal(_) => None, PanelSide::Top | PanelSide::Bottom => None,
}; };
let size_range: Rangef = match side { let outer_size_range: Rangef = match side {
PanelSide::Vertical(_) => Rangef::new(96.0, f32::INFINITY), PanelSide::Left | PanelSide::Right => Rangef::new(96.0, f32::INFINITY),
PanelSide::Horizontal(_) => Rangef::new(20.0, f32::INFINITY), PanelSide::Top | PanelSide::Bottom => Rangef::new(20.0, f32::INFINITY),
}; };
Self { Self {
@@ -364,8 +227,8 @@ impl Panel {
frame: None, frame: None,
resizable: true, resizable: true,
show_separator_line: true, show_separator_line: true,
default_size, default_outer_size,
size_range, outer_size_range,
} }
} }
@@ -401,10 +264,10 @@ impl Panel {
/// The initial wrapping width of the [`Panel`], including margins. /// The initial wrapping width of the [`Panel`], including margins.
#[inline] #[inline]
pub fn default_size(mut self, default_size: f32) -> Self { pub fn default_size(mut self, default_size: f32) -> Self {
self.default_size = Some(default_size); self.default_outer_size = Some(default_size);
self.size_range = Rangef::new( self.outer_size_range = Rangef::new(
self.size_range.min.at_most(default_size), self.outer_size_range.min.at_most(default_size),
self.size_range.max.at_least(default_size), self.outer_size_range.max.at_least(default_size),
); );
self self
} }
@@ -412,14 +275,14 @@ impl Panel {
/// Minimum size of the panel, including margins. /// Minimum size of the panel, including margins.
#[inline] #[inline]
pub fn min_size(mut self, min_size: f32) -> Self { pub fn min_size(mut self, min_size: f32) -> Self {
self.size_range = Rangef::new(min_size, self.size_range.max.at_least(min_size)); self.outer_size_range = Rangef::new(min_size, self.outer_size_range.max.at_least(min_size));
self self
} }
/// Maximum size of the panel, including margins. /// Maximum size of the panel, including margins.
#[inline] #[inline]
pub fn max_size(mut self, max_size: f32) -> Self { pub fn max_size(mut self, max_size: f32) -> Self {
self.size_range = Rangef::new(self.size_range.min.at_most(max_size), max_size); self.outer_size_range = Rangef::new(self.outer_size_range.min.at_most(max_size), max_size);
self self
} }
@@ -427,18 +290,18 @@ impl Panel {
#[inline] #[inline]
pub fn size_range(mut self, size_range: impl Into<Rangef>) -> Self { pub fn size_range(mut self, size_range: impl Into<Rangef>) -> Self {
let size_range = size_range.into(); let size_range = size_range.into();
self.default_size = self self.default_outer_size = self
.default_size .default_outer_size
.map(|default_size| clamp_to_range(default_size, size_range)); .map(|default_size| clamp_to_range(default_size, size_range));
self.size_range = size_range; self.outer_size_range = size_range;
self self
} }
/// Enforce this exact size, including margins. /// Enforce this exact size, including margins.
#[inline] #[inline]
pub fn exact_size(mut self, size: f32) -> Self { pub fn exact_size(mut self, size: f32) -> Self {
self.default_size = Some(size); self.default_outer_size = Some(size);
self.size_range = Rangef::point(size); self.outer_size_range = Rangef::point(size);
self self
} }
@@ -469,23 +332,25 @@ impl Panel {
is_expanded: bool, is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> { ) -> Option<InnerResponse<R>> {
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); let how_expanded = animate_expansion(ui, self.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate if how_expanded == 0.0 {
let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else {
// Make sure the ids of the next widgets are the same whether we show the panel or not: // Make sure the ids of the next widgets are the same whether we show the panel or not:
ui.skip_ahead_auto_ids(1); ui.skip_ahead_auto_ids(1);
return None; return None;
}; }
if how_expanded < 1.0 { if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state: // Show a fake panel in this in-between animation state:
animated_panel.show_inside(ui, |_ui| {}); // TODO(emilk): move the panel out-of-screen instead of changing its width.
None // Then we can actually paint it as it animates.
} else { let fake_size = how_expanded * self.outer_size(ui);
// Show the real panel: self.into_fake_animating(fake_size)
Some(animated_panel.show_inside(ui, add_contents)) .show_inside(ui, |_ui| {});
return None;
} }
Some(self.show_inside(ui, add_contents))
} }
/// Show either a collapsed or a expanded panel, with a nice animation between. /// Show either a collapsed or a expanded panel, with a nice animation between.
@@ -496,24 +361,20 @@ impl Panel {
expanded_panel: Self, expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R, add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let how_expanded = let how_expanded = animate_expansion(ui, expanded_panel.id.with("animation"), is_expanded);
animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
let animated_between_panel = Self::get_animated_between_panel( let panel = if how_expanded == 0.0 {
ui.ctx(), collapsed_panel
is_expanded,
collapsed_panel,
expanded_panel,
);
if 0.0 == how_expanded {
animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
} else if how_expanded < 1.0 { } else if how_expanded < 1.0 {
// Show animation: let collapsed_size = collapsed_panel.outer_size(ui);
animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) let expanded_size = expanded_panel.outer_size(ui);
let fake_size = lerp(collapsed_size..=expanded_size, how_expanded);
expanded_panel.into_fake_animating(fake_size)
} else { } else {
animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) expanded_panel
} };
panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
} }
} }
@@ -522,157 +383,159 @@ impl Panel {
/// Show the panel inside a [`Ui`]. /// Show the panel inside a [`Ui`].
fn show_inside_dyn<'c, R>( fn show_inside_dyn<'c, R>(
self, self,
ui: &mut Ui, parent_ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let side = self.side; let side = self.side;
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 size_range = self.size_range; let outer_size_range = self.outer_size_range;
// Define the sizing of the panel. let frame = self
let mut panel_sizer = PanelSizer::new(&self, ui); .frame
.unwrap_or_else(|| Frame::side_top_panel(parent_ui.style()));
let available_rect = parent_ui.available_rect_before_wrap();
let mut outer_size = self.initial_outer_size(parent_ui, frame);
let mut outer_rect = self.compute_outer_rect(available_rect, outer_size);
// Check for duplicate id // Check for duplicate id
ui.ctx() parent_ui.check_for_id_clash(id, outer_rect, "Panel");
.check_for_id_clash(id, panel_sizer.panel_rect, "Panel");
if self.resizable { if resizable {
// Prepare the resizable panel to avoid frame latency in the resize // Resolve the resize interaction first to avoid frame latency in the resize.
self.prepare_resizable_panel(&mut panel_sizer, ui); let resize_id = id.with("__resize");
if let Some(resize_response) = parent_ui.read_response(resize_id)
&& resize_response.dragged()
&& let Some(pointer) = resize_response.interact_pointer_pos()
{
let axis = side.axis();
outer_size = (pointer[axis] - side.fixed_pos(outer_rect)).abs();
outer_size = clamp_to_range(outer_size, outer_size_range)
.at_most(available_rect.size_along(axis));
side.set_rect_size(&mut outer_rect, outer_size);
}
} }
// NOTE(shark98): This must be **after** the resizable preparation, as the size // NOTE(shark98): This must be **after** the resizable preparation, as the size
// may change and round_ui() uses the size. // may change and round_ui() uses the size.
panel_sizer.panel_rect = panel_sizer.panel_rect.round_ui(); outer_rect = outer_rect.round_ui();
let mut panel_ui = ui.new_child( let mut panel_ui = parent_ui.new_child(
UiBuilder::new() UiBuilder::new()
.id_salt(id) .id_salt(id)
.ui_stack_info(UiStackInfo::new(side.ui_kind())) .ui_stack_info(UiStackInfo::new(side.ui_kind()))
.max_rect(panel_sizer.panel_rect) .max_rect(outer_rect)
.layout(Layout::top_down(Align::Min)), .layout(Layout::top_down(Align::Min)),
); );
panel_ui.expand_to_include_rect(panel_sizer.panel_rect); panel_ui.expand_to_include_rect(outer_rect);
panel_ui.set_clip_rect(panel_sizer.panel_rect); // If we overflow, don't do so visibly (#4475) panel_ui.set_clip_rect(outer_rect); // If we overflow, don't do so visibly (#4475)
let inner_response = panel_sizer.frame.show(&mut panel_ui, |ui| { let axis = side.axis();
match side { let panel_axis_min =
PanelSide::Vertical(_) => { (outer_size_range.min - frame.total_margin().sum()[axis]).at_least(0.0);
ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height let inner_response = frame.show(&mut panel_ui, |content_ui| {
ui.set_min_width( // Make sure the frame fills the cross-axis fully:
(size_range.min - panel_sizer.frame.inner_margin.sum().x).at_least(0.0), let cross_axis_size = content_ui.max_rect().size_along(side.cross_axis());
); if axis == 0 {
} content_ui.set_min_height(cross_axis_size);
PanelSide::Horizontal(_) => { content_ui.set_min_width(panel_axis_min);
ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width } else {
ui.set_min_height( content_ui.set_min_width(cross_axis_size);
(size_range.min - panel_sizer.frame.inner_margin.sum().y).at_least(0.0), content_ui.set_min_height(panel_axis_min);
);
}
} }
add_contents(ui) add_contents(content_ui)
}); });
let rect = inner_response.response.rect; // `Frame::show` returns the _outer_ rect (including margin & border).
let outer_rect = inner_response.response.rect;
{ {
let mut cursor = ui.cursor(); let mut cursor = parent_ui.cursor();
match side { match side {
PanelSide::Vertical(side) => match side { PanelSide::Left | PanelSide::Top => {
VerticalSide::Left => cursor.min.x = rect.max.x, cursor.min[axis] = outer_rect.max[axis];
VerticalSide::Right => cursor.max.x = rect.min.x, }
}, PanelSide::Right | PanelSide::Bottom => {
PanelSide::Horizontal(side) => match side { cursor.max[axis] = outer_rect.min[axis];
HorizontalSide::Top => cursor.min.y = rect.max.y, }
HorizontalSide::Bottom => cursor.max.y = rect.min.y,
},
} }
ui.set_cursor(cursor); parent_ui.set_cursor(cursor);
} }
ui.expand_to_include_rect(rect); parent_ui.expand_to_include_rect(outer_rect);
let mut resize_hover = false; let (resize_hover, is_resizing) = if resizable {
let mut is_resizing = false;
if resizable {
// 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.
(resize_hover, is_resizing) = self.resize_panel(&panel_sizer, ui); self.resize_panel(outer_rect, parent_ui)
} } else {
(false, false)
};
if resize_hover || is_resizing { if resize_hover || is_resizing {
ui.set_cursor_icon(self.cursor_icon(&panel_sizer)); parent_ui.set_cursor_icon(self.cursor_icon(outer_size));
} }
PanelState { rect }.store(ui.ctx(), id); PanelState { outer_rect }.store(parent_ui, id);
{ {
let stroke = if is_resizing { let stroke = if is_resizing {
ui.style().visuals.widgets.active.fg_stroke // highly visible parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover { } else if resize_hover {
ui.style().visuals.widgets.hovered.fg_stroke // highly visible parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line { } else if show_separator_line {
// TODO(emilk): distinguish resizable from non-resizable // TODO(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke // dim parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else { } else {
Stroke::NONE Stroke::NONE
}; };
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
match side { let line_pos = side.resize_pos(outer_rect) + 0.5 * side.sign() * stroke.width;
PanelSide::Vertical(side) => { let cross_range = outer_rect.range_along(side.cross_axis());
let x = side.opposite().side_x(rect) + 0.5 * side.sign() * stroke.width; if axis == 0 {
ui.painter() parent_ui.painter().vline(line_pos, cross_range, stroke);
.vline(x, panel_sizer.panel_rect.y_range(), stroke); } else {
} parent_ui.painter().hline(cross_range, line_pos, stroke);
PanelSide::Horizontal(side) => {
let y = side.opposite().side_y(rect) + 0.5 * side.sign() * stroke.width;
ui.painter()
.hline(panel_sizer.panel_rect.x_range(), y, stroke);
}
} }
} }
inner_response inner_response
} }
fn prepare_resizable_panel(&self, panel_sizer: &mut PanelSizer<'_>, ui: &Ui) { /// Outer size to start the frame with: from persisted state, or a sensible default.
let resize_id = self.id.with("__resize"); fn initial_outer_size(&self, ui: &Ui, frame: Frame) -> f32 {
let resize_response = ui.ctx().read_response(resize_id); let axis = self.side.axis();
if let Some(state) = PanelState::load(ui, self.id) {
if let Some(resize_response) = resize_response { state.outer_rect.size_along(axis)
// NOTE(sharky98): The original code was initializing to } else {
// false first, but it doesn't seem necessary. self.default_outer_size.unwrap_or_else(|| {
let is_resizing = resize_response.dragged(); ui.style().spacing.interact_size[axis] + frame.total_margin().sum()[axis]
let pointer = resize_response.interact_pointer_pos(); })
panel_sizer.prepare_resizing_response(is_resizing, pointer);
} }
} }
fn resize_panel(&self, panel_sizer: &PanelSizer<'_>, ui: &Ui) -> (bool, bool) { /// Clamp `outer_size` to the allowed range / available space, then compute the panel rect.
let (resize_x, resize_y, amount): (Rangef, Rangef, Vec2) = match self.side { fn compute_outer_rect(&self, available_rect: Rect, mut outer_size: f32) -> Rect {
PanelSide::Vertical(side) => { let mut outer_rect = available_rect;
let resize_x = side.opposite().side_x(panel_sizer.panel_rect); outer_size = clamp_to_range(outer_size, self.outer_size_range)
let resize_y = panel_sizer.panel_rect.y_range(); .at_most(available_rect.size_along(self.side.axis()));
( self.side.set_rect_size(&mut outer_rect, outer_size);
Rangef::from(resize_x..=resize_x), outer_rect
resize_y, }
vec2(ui.style().interaction.resize_grab_radius_side, 0.0),
) fn resize_panel(&self, outer_rect: Rect, ui: &Ui) -> (bool, bool) {
} let resize_pos = self.side.resize_pos(outer_rect);
PanelSide::Horizontal(side) => { let panel_axis_range = Rangef::point(resize_pos);
let resize_x = panel_sizer.panel_rect.x_range(); let cross_range = outer_rect.range_along(self.side.cross_axis());
let resize_y = side.opposite().side_y(panel_sizer.panel_rect); let (resize_x, resize_y) = if self.side.axis() == 0 {
( (panel_axis_range, cross_range)
resize_x, } else {
Rangef::from(resize_y..=resize_y), (cross_range, panel_axis_range)
vec2(0.0, ui.style().interaction.resize_grab_radius_side),
)
}
}; };
let amount = ui.style().interaction.resize_grab_radius_side * self.side.axis_unit();
let resize_id = self.id.with("__resize"); let resize_id = 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);
@@ -681,107 +544,55 @@ impl Panel {
(resize_response.hovered(), resize_response.dragged()) (resize_response.hovered(), resize_response.dragged())
} }
fn cursor_icon(&self, panel_sizer: &PanelSizer<'_>) -> CursorIcon { fn cursor_icon(&self, outer_size: f32) -> CursorIcon {
if panel_sizer.size <= self.size_range.min { if outer_size <= self.outer_size_range.min {
// Can only grow (toward the resizable side):
match self.side { match self.side {
PanelSide::Vertical(side) => match side { PanelSide::Left => CursorIcon::ResizeEast,
VerticalSide::Left => CursorIcon::ResizeEast, PanelSide::Right => CursorIcon::ResizeWest,
VerticalSide::Right => CursorIcon::ResizeWest, PanelSide::Top => CursorIcon::ResizeSouth,
}, PanelSide::Bottom => CursorIcon::ResizeNorth,
PanelSide::Horizontal(side) => match side {
HorizontalSide::Top => CursorIcon::ResizeSouth,
HorizontalSide::Bottom => CursorIcon::ResizeNorth,
},
} }
} else if panel_sizer.size < self.size_range.max { } else if outer_size < self.outer_size_range.max {
match self.side { if self.side.axis() == 0 {
PanelSide::Vertical(_) => CursorIcon::ResizeHorizontal, CursorIcon::ResizeHorizontal
PanelSide::Horizontal(_) => CursorIcon::ResizeVertical, } else {
CursorIcon::ResizeVertical
} }
} else { } else {
// Can only shrink (toward the fixed side):
match self.side { match self.side {
PanelSide::Vertical(side) => match side { PanelSide::Left => CursorIcon::ResizeWest,
VerticalSide::Left => CursorIcon::ResizeWest, PanelSide::Right => CursorIcon::ResizeEast,
VerticalSide::Right => CursorIcon::ResizeEast, PanelSide::Top => CursorIcon::ResizeNorth,
}, PanelSide::Bottom => CursorIcon::ResizeSouth,
PanelSide::Horizontal(side) => match side {
HorizontalSide::Top => CursorIcon::ResizeNorth,
HorizontalSide::Bottom => CursorIcon::ResizeSouth,
},
} }
} }
} }
/// Get the real or fake panel to animate if `is_expanded` is `true`. /// Build a non-resizable, fixed-size clone of this panel for animating between sizes.
fn get_animated_panel(self, ctx: &Context, is_expanded: bool) -> Option<Self> { ///
let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); /// Uses a distinct id so the resulting panel doesn't clash with the real one.
fn into_fake_animating(self, outer_size: f32) -> Self {
if 0.0 == how_expanded { Self {
None id: self.id.with("animating_panel"),
} else if how_expanded < 1.0 { ..self
// Show a fake panel in this in-between animation state:
// TODO(emilk): move the panel out-of-screen instead of changing its width.
// Then we can actually paint it as it animates.
let expanded_size = Self::animated_size(ctx, &self);
let fake_size = how_expanded * expanded_size;
Some(
Self {
id: self.id.with("animating_panel"),
..self
}
.resizable(false)
.exact_size(fake_size),
)
} else {
// Show the real panel:
Some(self)
} }
.resizable(false)
.exact_size(outer_size)
} }
/// Get either the collapsed or expended panel to animate. /// Get the current _outer_ width or height of the panel (from previous frame),
fn get_animated_between_panel( /// including the [`Frame`] margin & border,
ctx: &Context, /// or fall back to some default.
is_expanded: bool, fn outer_size(&self, ctx: &Context) -> f32 {
collapsed_panel: Self, let axis = self.side.axis();
expanded_panel: Self, if let Some(state) = PanelState::load(ctx, self.id) {
) -> Self { state.outer_rect.size_along(axis)
let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
if 0.0 == how_expanded {
collapsed_panel
} else if how_expanded < 1.0 {
let collapsed_size = Self::animated_size(ctx, &collapsed_panel);
let expanded_size = Self::animated_size(ctx, &expanded_panel);
let fake_size = lerp(collapsed_size..=expanded_size, how_expanded);
Self {
id: expanded_panel.id.with("animating_panel"),
..expanded_panel
}
.resizable(false)
.exact_size(fake_size)
} else { } else {
expanded_panel ctx.global_style().spacing.interact_size[axis]
} }
} }
fn animated_size(ctx: &Context, panel: &Self) -> f32 {
let get_rect_state_size = |state: PanelState| match panel.side {
PanelSide::Vertical(_) => state.rect.width(),
PanelSide::Horizontal(_) => state.rect.height(),
};
let get_spacing_size = || match panel.side {
PanelSide::Vertical(_) => ctx.global_style().spacing.interact_size.x,
PanelSide::Horizontal(_) => ctx.global_style().spacing.interact_size.y,
};
PanelState::load(ctx, panel.id)
.map(get_rect_state_size)
.or(panel.default_size)
.unwrap_or_else(get_spacing_size)
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -854,14 +665,14 @@ impl CentralPanel {
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let Self { frame } = self; let Self { frame } = self;
let panel_rect = ui.available_rect_before_wrap(); let outer_rect = ui.available_rect_before_wrap();
let mut panel_ui = ui.new_child( let mut panel_ui = ui.new_child(
UiBuilder::new() UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::CentralPanel)) .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
.max_rect(panel_rect) .max_rect(outer_rect)
.layout(Layout::top_down(Align::Min)), .layout(Layout::top_down(Align::Min)),
); );
panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) panel_ui.set_clip_rect(outer_rect); // If we overflow, don't do so visibly (#4475)
let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style())); let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
let response = frame.show(&mut panel_ui, |ui| { let response = frame.show(&mut panel_ui, |ui| {

View File

@@ -476,6 +476,32 @@ impl Rect {
Rangef::new(self.min.y, self.max.y) Rangef::new(self.min.y, self.max.y)
} }
/// The extent along the given axis: `0` for x, `1` for y.
///
/// Equivalent to [`Self::x_range`] for `axis == 0` and [`Self::y_range`] for `axis == 1`.
///
/// # Panics
/// If `axis` is not `0` or `1`.
#[inline]
pub fn range_along(&self, axis: usize) -> Rangef {
match axis {
0 => self.x_range(),
1 => self.y_range(),
_ => panic!("axis must be 0 or 1, got {axis}"),
}
}
/// The size along the given axis: `0` for x (width), `1` for y (height).
///
/// Equivalent to `self.size()[axis]`.
///
/// # Panics
/// If `axis` is not `0` or `1`.
#[inline]
pub fn size_along(&self, axis: usize) -> f32 {
self.size()[axis]
}
#[inline(always)] #[inline(always)]
pub fn bottom_up_range(&self) -> Rangef { pub fn bottom_up_range(&self) -> Rangef {
Rangef::new(self.max.y, self.min.y) Rangef::new(self.max.y, self.min.y)