use emath::GuiRounding as _;
use crate::{
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
ctx.animate_bool_responsive(id, is_expanded)
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PanelState {
#[cfg_attr(feature = "serde", serde(alias = "rect"))]
pub outer_rect: Rect,
}
impl PanelState {
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data_mut(|d| d.get_persisted(bar_id))
}
pub fn size(&self) -> Vec2 {
self.outer_rect.size()
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PanelSide {
Left,
Right,
Top,
Bottom,
}
impl PanelSide {
fn axis(self) -> usize {
match self {
Self::Left | Self::Right => 0,
Self::Top | Self::Bottom => 1,
}
}
fn cross_axis(self) -> usize {
1 - self.axis()
}
fn axis_unit(self) -> Vec2 {
match self {
Self::Left | Self::Right => Vec2::X,
Self::Top | Self::Bottom => Vec2::Y,
}
}
fn dir_vec2(self) -> Vec2 {
self.sign() * self.axis_unit()
}
fn sign(self) -> f32 {
match self {
Self::Left | Self::Top => -1.0,
Self::Right | Self::Bottom => 1.0,
}
}
fn fixed_pos(self, rect: Rect) -> f32 {
match self {
Self::Left => rect.left(),
Self::Right => rect.right(),
Self::Top => rect.top(),
Self::Bottom => rect.bottom(),
}
}
fn resize_pos(self, rect: Rect) -> f32 {
match self {
Self::Left => rect.right(),
Self::Right => rect.left(),
Self::Top => rect.bottom(),
Self::Bottom => rect.top(),
}
}
fn set_rect_size(self, rect: &mut Rect, size: f32) {
match self {
Self::Left => rect.max.x = rect.min.x + 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 {
match self {
Self::Left => UiKind::LeftPanel,
Self::Right => UiKind::RightPanel,
Self::Top => UiKind::TopPanel,
Self::Bottom => UiKind::BottomPanel,
}
}
}
#[must_use = "You should call .show()"]
pub struct Panel {
side: PanelSide,
id: Id,
frame: Option<Frame>,
resizable: bool,
show_separator_line: bool,
default_outer_size: Option<f32>,
outer_size_range: Rangef,
slide_fraction: f32,
resize_id_source: Option<Id>,
collapse_threshold: Option<f32>,
}
impl Panel {
pub fn left(id: impl Into<Id>) -> Self {
Self::new(PanelSide::Left, id)
}
pub fn right(id: impl Into<Id>) -> Self {
Self::new(PanelSide::Right, id)
}
pub fn top(id: impl Into<Id>) -> Self {
Self::new(PanelSide::Top, id).resizable(false)
}
pub fn bottom(id: impl Into<Id>) -> Self {
Self::new(PanelSide::Bottom, id).resizable(false)
}
fn new(side: PanelSide, id: impl Into<Id>) -> Self {
let default_outer_size: Option<f32> = match side {
PanelSide::Left | PanelSide::Right => Some(200.0),
PanelSide::Top | PanelSide::Bottom => None,
};
let outer_size_range: Rangef = match side {
PanelSide::Left | PanelSide::Right => Rangef::new(96.0, f32::INFINITY),
PanelSide::Top | PanelSide::Bottom => Rangef::new(20.0, f32::INFINITY),
};
Self {
side,
id: id.into(),
frame: None,
resizable: true,
show_separator_line: true,
default_outer_size,
outer_size_range,
slide_fraction: 1.0,
resize_id_source: None,
collapse_threshold: None,
}
}
#[inline]
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
#[inline]
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
self.show_separator_line = show_separator_line;
self
}
#[inline]
pub fn default_size(mut self, default_size: f32) -> Self {
self.default_outer_size = Some(default_size);
self.outer_size_range = Rangef::new(
self.outer_size_range.min.at_most(default_size),
self.outer_size_range.max.at_least(default_size),
);
self
}
#[inline]
pub fn min_size(mut self, min_size: f32) -> Self {
self.outer_size_range = Rangef::new(min_size, self.outer_size_range.max.at_least(min_size));
self
}
#[inline]
pub fn max_size(mut self, max_size: f32) -> Self {
self.outer_size_range = Rangef::new(self.outer_size_range.min.at_most(max_size), max_size);
self
}
#[inline]
pub fn size_range(mut self, size_range: impl Into<Rangef>) -> Self {
let size_range = size_range.into();
self.default_outer_size = self
.default_outer_size
.map(|default_size| clamp_to_range(default_size, size_range));
self.outer_size_range = size_range;
self
}
#[inline]
pub fn exact_size(mut self, size: f32) -> Self {
self.default_outer_size = Some(size);
self.outer_size_range = Rangef::point(size);
self
}
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl Panel {
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.show_inside_dyn(ui, None, Box::new(add_contents))
}
#[deprecated = "Renamed to `show`"]
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show(ui, add_contents)
}
pub fn show_collapsible<R>(
self,
ui: &mut Ui,
is_expanded: &mut bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
if how_expanded == 0.0 {
self.keep_drag_alive_for_reopen(ui, is_expanded);
ui.skip_ahead_auto_ids(1);
return None;
}
let drag_in_progress = ui
.read_response(self.id.with("__resize"))
.is_some_and(|r| r.dragged());
let panel = if how_expanded < 1.0 {
if drag_in_progress {
self.with_slide_fraction(how_expanded)
} else {
self.with_slide_fraction(how_expanded).resizable(false) }
} else {
self
};
Some(panel.show_inside_dyn(ui, Some(is_expanded), Box::new(add_contents)))
}
#[deprecated = "Renamed to `show_collapsible`"]
pub fn show_animated_inside<R>(
self,
ui: &mut Ui,
mut is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
self.show_collapsible(ui, &mut is_expanded, add_contents)
}
pub fn show_switched<R>(
ui: &mut Ui,
is_expanded: &mut bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, bool) -> R,
) -> InnerResponse<R> {
debug_assert!(
collapsed_panel.id != expanded_panel.id,
"show_switched: the collapsed and expanded panels must have distinct ids \
(their persisted sizes are stored per-id, and sharing one id would let the collapsed \
size overwrite the expanded size)."
);
let resize_id_source = expanded_panel.id;
let collapse_threshold = collapsed_panel.outer_size(ui);
let drag_in_progress = ui
.read_response(resize_id_source.with("__resize"))
.is_some_and(|r| r.dragged());
let animation_id = expanded_panel.id.with("animation");
let how_expanded = if drag_in_progress {
ui.animate_bool_with_time(animation_id, *is_expanded, 0.0)
} else {
animate_expansion(ui, animation_id, *is_expanded)
};
let show_expanded_contents = *is_expanded || 0.5 < how_expanded;
if how_expanded == 0.0 {
collapsed_panel
.with_resize_id_source(resize_id_source)
.show_inside_dyn(
ui,
Some(is_expanded),
Box::new(|ui| add_contents(ui, false)),
)
} else {
let expanded_panel = expanded_panel.with_collapse_threshold(collapse_threshold);
let panel = if how_expanded < 1.0 {
let expanded_size = expanded_panel.outer_size(ui);
let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded);
let slide_fraction = if 0.0 < expanded_size {
visible_size / expanded_size
} else {
1.0
};
let panel = expanded_panel.with_slide_fraction(slide_fraction);
if drag_in_progress {
panel
} else {
panel.resizable(false) }
} else {
expanded_panel
};
panel.show_inside_dyn(
ui,
Some(is_expanded),
Box::new(|ui| add_contents(ui, show_expanded_contents)),
)
}
}
#[deprecated = "Renamed to `show_switched`"]
pub fn show_animated_between_inside<R>(
ui: &mut Ui,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> InnerResponse<R> {
let mut is_expanded = is_expanded;
Self::show_switched(
ui,
&mut is_expanded,
collapsed_panel,
expanded_panel,
|ui, expanded| add_contents(ui, if expanded { 1.0 } else { 0.0 }),
)
}
}
impl Panel {
fn show_inside_dyn<'c, R>(
mut self,
parent_ui: &mut Ui,
mut is_expanded: Option<&mut bool>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let side = self.side;
let id = self.id;
let resizable = self.resizable;
let show_separator_line = self.show_separator_line;
let available_rect = parent_ui.available_rect_before_wrap();
{
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 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
};
parent_ui.check_for_id_clash(id, outer_rect, "Panel");
let mut resize_drag_in_progress = false;
if resizable {
let resize_id = self.resize_id_source.unwrap_or(id).with("__resize");
let resize_response = parent_ui.read_response(resize_id);
if let Some(resize_response) = resize_response.as_ref()
&& resize_response.double_clicked()
&& let Some(is_expanded) = is_expanded.as_deref_mut()
{
*is_expanded = !*is_expanded;
}
if let Some(resize_response) = resize_response
&& (resize_response.dragged() || resize_response.drag_stopped())
&& let Some(pointer) = resize_response.interact_pointer_pos()
{
resize_drag_in_progress = resize_response.dragged();
let axis = side.axis();
let prev_outer_size = outer_size;
let raw_outer_size = -side.sign() * (pointer[axis] - side.fixed_pos(outer_rect));
outer_size = clamp_to_range(raw_outer_size, self.outer_size_range)
.at_most(available_rect.size_along(axis));
side.set_rect_size(&mut outer_rect, outer_size);
if let Some(is_expanded) = is_expanded {
let collapse_threshold =
self.collapse_threshold.unwrap_or(self.outer_size_range.min);
if raw_outer_size < collapse_threshold && raw_outer_size < prev_outer_size {
*is_expanded = false;
}
if self.outer_size_range.max < raw_outer_size {
*is_expanded = true;
}
}
}
}
outer_rect = outer_rect.round_ui();
let slide_distance = (1.0 - self.slide_fraction) * outer_size;
let shifted_outer_rect = if slide_distance == 0.0 {
outer_rect
} else {
outer_rect
.translate(slide_distance * side.dir_vec2())
.round_ui()
};
let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
let mut panel_ui = parent_ui.new_child(
UiBuilder::new()
.id_salt(id)
.ui_stack_info(UiStackInfo::new(side.ui_kind()))
.max_rect(shifted_outer_rect)
.layout(Layout::top_down(Align::Min)),
);
panel_ui.expand_to_include_rect(shifted_outer_rect);
panel_ui.set_clip_rect(visible_outer_rect);
let axis = side.axis();
let panel_axis_min =
(self.outer_size_range.min - frame.total_margin().sum()[axis]).at_least(0.0);
let mut inner_response = frame.show(&mut panel_ui, |content_ui| {
let cross_axis_size = content_ui.max_rect().size_along(side.cross_axis());
if axis == 0 {
content_ui.set_min_height(cross_axis_size);
content_ui.set_min_width(panel_axis_min);
} else {
content_ui.set_min_width(cross_axis_size);
content_ui.set_min_height(panel_axis_min);
}
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);
}
let shifted_outer_rect = inner_response.response.rect;
let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
{
let mut cursor = parent_ui.cursor();
match side {
PanelSide::Left | PanelSide::Top => {
cursor.min[axis] = visible_outer_rect.max[axis];
}
PanelSide::Right | PanelSide::Bottom => {
cursor.max[axis] = visible_outer_rect.min[axis];
}
}
parent_ui.set_cursor(cursor);
}
parent_ui.expand_to_include_rect(visible_outer_rect);
let (resize_hover, is_resizing) = if resizable {
let resize_response = self.resize_panel(shifted_outer_rect, parent_ui);
(resize_response.hovered(), resize_response.dragged())
} else {
(false, false)
};
if resize_hover || is_resizing {
parent_ui.set_cursor_icon(self.cursor_icon(outer_size));
}
let is_animating = 0.0 < self.slide_fraction && self.slide_fraction < 1.0;
if !resize_drag_in_progress && !is_animating || PanelState::load(parent_ui, id).is_none() {
PanelState {
outer_rect: shifted_outer_rect,
}
.store(parent_ui, id);
}
if 0.01 < self.slide_fraction {
let stroke = if is_resizing {
parent_ui.style().visuals.widgets.active.fg_stroke } else if resize_hover {
parent_ui.style().visuals.widgets.hovered.fg_stroke } else if show_separator_line {
parent_ui.style().visuals.widgets.noninteractive.bg_stroke } else {
Stroke::NONE
};
let line_pos = side.resize_pos(shifted_outer_rect) + 0.5 * side.sign() * stroke.width;
let cross_range = shifted_outer_rect.range_along(side.cross_axis());
if axis == 0 {
parent_ui.painter().vline(line_pos, cross_range, stroke);
} else {
parent_ui.painter().hline(cross_range, line_pos, stroke);
}
}
inner_response
}
fn resolve_frame(&self, ui: &Ui) -> Frame {
self.frame
.unwrap_or_else(|| Frame::side_top_panel(ui.style()))
}
fn keep_drag_alive_for_reopen(&self, ui: &Ui, is_expanded: &mut bool) {
let resize_id = self.id.with("__resize");
let Some(resize_response) = ui.read_response(resize_id) else {
return;
};
if !resize_response.dragged() {
return;
}
let Some(pointer) = resize_response.interact_pointer_pos() else {
return;
};
let available_rect = ui.available_rect_before_wrap();
let fixed_edge_pos = self.side.fixed_pos(available_rect);
let cross_range = available_rect.range_along(self.side.cross_axis());
let resize_rect = if self.side.axis() == 0 {
Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range)
} else {
Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
};
let grab = ui.style().interaction.resize_grab_radius_side;
let resize_rect = resize_rect.expand2(grab * self.side.axis_unit());
ui.interact(resize_rect, resize_id, Sense::drag());
ui.set_cursor_icon(self.cursor_icon(0.0));
let dragged_size = -self.side.sign() * (pointer[self.side.axis()] - fixed_edge_pos);
if self.outer_size_range.min < dragged_size {
*is_expanded = true;
}
}
fn outer_size(&self, ui: &Ui) -> f32 {
let axis = self.side.axis();
let raw = if let Some(state) = PanelState::load(ui, self.id) {
state.outer_rect.size_along(axis)
} else if let Some(default_outer_size) = self.default_outer_size {
default_outer_size
} else {
let frame = self.resolve_frame(ui);
ui.style().spacing.interact_size[axis] + frame.total_margin().sum()[axis]
};
clamp_to_range(raw, self.outer_size_range)
}
fn resize_panel(&self, outer_rect: Rect, ui: &Ui) -> Response {
let resize_pos = self.side.resize_pos(outer_rect);
let panel_axis_range = Rangef::point(resize_pos);
let cross_range = outer_rect.range_along(self.side.cross_axis());
let (resize_x, resize_y) = if self.side.axis() == 0 {
(panel_axis_range, cross_range)
} else {
(cross_range, panel_axis_range)
};
let amount = ui.style().interaction.resize_grab_radius_side * self.side.axis_unit();
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);
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
}
fn cursor_icon(&self, outer_size: f32) -> CursorIcon {
let can_drag_to_expand = self.resize_id_source.is_some();
let max_for_cursor = if can_drag_to_expand {
f32::INFINITY
} else {
self.outer_size_range.max
};
if outer_size <= self.outer_size_range.min {
match self.side {
PanelSide::Left => CursorIcon::ResizeEast,
PanelSide::Right => CursorIcon::ResizeWest,
PanelSide::Top => CursorIcon::ResizeSouth,
PanelSide::Bottom => CursorIcon::ResizeNorth,
}
} else if outer_size < max_for_cursor {
if self.side.axis() == 0 {
CursorIcon::ResizeHorizontal
} else {
CursorIcon::ResizeVertical
}
} else {
match self.side {
PanelSide::Left => CursorIcon::ResizeWest,
PanelSide::Right => CursorIcon::ResizeEast,
PanelSide::Top => CursorIcon::ResizeNorth,
PanelSide::Bottom => CursorIcon::ResizeSouth,
}
}
}
#[inline]
fn with_slide_fraction(mut self, slide_fraction: f32) -> Self {
self.slide_fraction = slide_fraction;
self
}
#[inline]
fn with_resize_id_source(mut self, id: Id) -> Self {
self.resize_id_source = Some(id);
self
}
#[inline]
fn with_collapse_threshold(mut self, threshold: f32) -> Self {
self.collapse_threshold = Some(threshold);
self
}
}
#[must_use = "You should call .show()"]
#[derive(Default)]
pub struct CentralPanel {
frame: Option<Frame>,
}
impl CentralPanel {
pub fn no_frame() -> Self {
Self {
frame: Some(Frame::NONE),
}
}
pub fn default_margins() -> Self {
Self { frame: None }
}
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
#[deprecated = "Renamed to `show`"]
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show(ui, add_contents)
}
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self { frame } = self;
let outer_rect = ui.available_rect_before_wrap();
let mut panel_ui = ui.new_child(
UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
.max_rect(outer_rect)
.layout(Layout::top_down(Align::Min)),
);
panel_ui.set_clip_rect(outer_rect);
let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
let response = frame.show(&mut panel_ui, |ui| {
ui.expand_to_include_rect(ui.max_rect()); add_contents(ui)
});
ui.advance_cursor_after_rect(response.response.rect);
response
}
}
fn clamp_to_range(x: f32, range: Rangef) -> f32 {
let range = range.as_positive();
x.clamp(range.min, range.max)
}