Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a Panel trait for SidePanel and TopBottomPanel #5643

Open
sharky98 opened this issue Jan 28, 2025 · 1 comment · May be fixed by #5659
Open

Introduce a Panel trait for SidePanel and TopBottomPanel #5643

sharky98 opened this issue Jan 28, 2025 · 1 comment · May be fixed by #5659

Comments

@sharky98
Copy link

Is your feature request related to a problem? Please describe.

I am trying to create a library around both SidePanel and TopBottomPanel so that they have the same behavior when used in a specific way (my specific case is toolbars and properties panels). Having two distinct type makes it hard to create such wrapper that abstract the specific of each of them.

In my use case, the toolbar will have a fixed width/height and will not be resizable. The use of the wrapper allows me to use a pre-made toolbar instead of duplicating options code.

Describe the solution you'd like

Seeing that the panels impl have almost the same signature, I think it make sense to create a trait so that we can create generic wrapper using impl trait type

Also, on a personal opinion, I think the name TopBottom and Side are a bit confusing at first glance, even more when nesting panels, making it seems that having a BottomPanel inside a TopPanel inside a LeftPanel is not possible. They could be renamed to HorizontalPanel (for the TopBottomPanel) and VerticalPanel (for the SidePanel). That way, no matter the nesting they'll always be horizontal or vertical.

The changes would be in crates/egui/src/containers/panel.rs

I can create a pull requests, but chances that the changes I made are not working as-is (excluding the breaking change of the names), me being a noob in Rust 😉

Draft code change proposal
//! Panels are [`Ui`] regions taking up e.g. the left side of a [`Ui`] or screen.
//!
//! Panels can either be a child of a [`Ui`] (taking up a portion of the parent)
//! or be top-level (taking up a portion of the whole screen).
//!
//! Together with [`crate::Window`] and [`crate::Area`]:s, top-level panels are
//! the only places where you can put your widgets.
//!
//! The order in which you add panels matter!
//! The first panel you add will always be the outermost, and the last you add will always be the innermost.
//!
//! You must never open one top-level panel from within another panel. Add one panel, then the next.
//!
//! ⚠ Always add any [`CentralPanel`] last.
//!
//! Add your [`crate::Window`]:s after any top-level panels.

use crate::{
    lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt,
    Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
};

fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
    ctx.animate_bool_responsive(id, is_expanded)
}

/// State regarding panels.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PanelState {
    pub rect: Rect,
}

impl PanelState {
    pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
        ctx.data_mut(|d| d.get_persisted(bar_id))
    }

    /// The size of the panel (from previous frame).
    pub fn size(&self) -> Vec2 {
        self.rect.size()
    }

    fn store(self, ctx: &Context, bar_id: Id) {
        ctx.data_mut(|d| d.insert_persisted(bar_id, self));
    }
}

// ----------------------------------------------------------------------------

pub trait Side {
    fn opposite(self) -> Self;
}

pub trait PanelOptions {
    /// The id should be globally unique, e.g. `Id::new("my_panel")`.
    fn new(side: impl Side, id: impl Into<Id>) -> Self;

    /// Can panel be resized by dragging the edge of it?
    ///
    /// Default is `true`.
    ///
    /// If you want your panel to be resizable you also need a widget in it that
    /// takes up more space as you resize it, such as:
    /// * Wrapping text ([`Ui::horizontal_wrapped`]).
    /// * A [`crate::ScrollArea`].
    /// * A [`crate::Separator`].
    /// * A [`crate::TextEdit`].
    /// * …
    fn resizable(mut self, resizable: bool) -> Self;

    /// Show a separator line, even when not interacting with it?
    ///
    /// Default: `true`.
    fn show_separator_line(mut self, show_separator_line: bool) -> Self;

    /// Change the background color, margins, etc.
    fn frame(mut self, frame: Frame) -> Self;
}

pub trait PanelSize {
    /// The initial wrapping size (width or height) of the panel including margins.
    fn default_size(mut self, default_size: f32) -> Self;

    /// Minimum size (width or height) of the panel, including margins.
    fn min_size(mut self, min_size: f32) -> Self;

    /// Maximum size (width or height) of the panel, including margins.
    fn max_size(mut self, max_size: f32) -> Self;

    /// The allowable size (width or height) range for the panel, including margins.
    fn size_range(mut self, size_range: impl Into<Rangef>) -> Self;

    /// Enforce this exact size (width or height), including margins.
    fn exact_size(mut self, size: f32) -> Self;
}

pub trait PanelActions {
    /// Show the panel inside a [`Ui`].
    fn show_inside<R>(
        self,
        ui: &mut Ui,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R>;

    /// Show the panel at the top level.
    fn show<R>(self, ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R>;

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated<R>(
        self,
        ctx: &Context,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>>;

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated_inside<R>(
        self,
        ui: &mut Ui,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>>;

    /// Show either a collapsed or an expanded panel, with a nice animation between.
    fn show_animated_between<R>(
        ctx: &Context,
        is_expanded: bool,
        collapsed_panel: Self,
        expanded_panel: Self,
        add_contents: impl FnOnce(&mut Ui, f32) -> R,
    ) -> Option<InnerResponse<R>>;

    /// Show either a collapsed or an expanded panel, with a nice animation between.
    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>;
}

// ----------------------------------------------------------------------------

/// [`Left`](VerticalSide::Left) or [`Right`](VerticalSide::Right)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VerticalSide {
    Left,
    Right,
}

impl Side for VerticalSide {
    fn opposite(self) -> Self {
        match self {
            Self::Left => Self::Right,
            Self::Right => Self::Left,
        }
    }
}

impl VerticalSide {
    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 side_x(self, rect: Rect) -> f32 {
        match self {
            Self::Left => rect.left(),
            Self::Right => rect.right(),
        }
    }
}

/// A panel that covers the entire left or right side of a [`Ui`] or screen.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ Always add any [`CentralPanel`] last.
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| {
///    ui.label("Hello World!");
/// });
/// # });
/// ```
///
/// See also [`HorizontalPanel`].
#[must_use = "You should call .show()"]
pub struct VerticalPanel {
    side: VerticalSide,
    id: Id,
    frame: Option<Frame>,
    resizable: bool,
    show_separator_line: bool,
    default_width: f32,
    width_range: Rangef,
}

impl PanelOptions for VerticalPanel {
    /// The id should be globally unique, e.g. `Id::new("my_panel")`.
    fn new(side: VerticalSide, id: impl Into<Id>) -> Self {
        Self {
            side,
            id: id.into(),
            frame: None,
            resizable: true,
            show_separator_line: true,
            default_width: 200.0,
            width_range: Rangef::new(96.0, f32::INFINITY),
        }
    }

    #[inline]
    fn resizable(mut self, resizable: bool) -> Self {
        self.resizable = resizable;
        self
    }

    #[inline]
    fn show_separator_line(mut self, show_separator_line: bool) -> Self {
        self.show_separator_line = show_separator_line;
        self
    }

    #[inline]
    fn frame(mut self, frame: Frame) -> Self {
        self.frame = Some(frame);
        self
    }
}

impl PanelSize for VerticalPanel {
    fn default_size(mut self, default_size: f32) -> Self {
        self.default_width(default_size)
    }

    fn min_size(mut self, min_size: f32) -> Self {
        self.min_width(min_size)
    }

    fn max_size(mut self, max_size: f32) -> Self {
        self.max_width(max_size)
    }

    fn size_range(mut self, size_range: impl Into<Rangef>) -> Self {
        self.width_range(size_range)
    }

    fn exact_size(mut self, size: f32) -> Self {
        self.exact_width(size)
    }
}

impl VerticalPanel {
    /// The id should be globally unique, e.g. `Id::new("my_left_panel")`.
    pub fn left(id: impl Into<Id>) -> Self {
        Self::new(VerticalSide::Left, id)
    }

    /// The id should be globally unique, e.g. `Id::new("my_right_panel")`.
    pub fn right(id: impl Into<Id>) -> Self {
        Self::new(VerticalSide::Right, id)
    }

    /// The initial wrapping width of the [`VerticalPanel`], including margins.
    #[inline]
    pub fn default_width(mut self, default_width: f32) -> Self {
        self.default_width = default_width;
        self.width_range = Rangef::new(
            self.width_range.min.at_most(default_width),
            self.width_range.max.at_least(default_width),
        );
        self
    }

    /// Minimum width of the panel, including margins.
    #[inline]
    pub fn min_width(mut self, min_width: f32) -> Self {
        self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
        self
    }

    /// Maximum width of the panel, including margins.
    #[inline]
    pub fn max_width(mut self, max_width: f32) -> Self {
        self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
        self
    }

    /// The allowable width range for the panel, including margins.
    #[inline]
    pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
        let width_range = width_range.into();
        self.default_width = clamp_to_range(self.default_width, width_range);
        self.width_range = width_range;
        self
    }

    /// Enforce this exact width, including margins.
    #[inline]
    pub fn exact_width(mut self, width: f32) -> Self {
        self.default_width = width;
        self.width_range = Rangef::point(width);
        self
    }
}

impl PanelActions for VerticalPanel {
    /// Show the panel inside a [`Ui`].
    fn show_inside<R>(
        self,
        ui: &mut Ui,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R> {
        self.show_inside_dyn(ui, Box::new(add_contents))
    }

    /// Show the panel at the top level.
    fn show<R>(self, ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
        self.show_dyn(ctx, Box::new(add_contents))
    }

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated<R>(
        self,
        ctx: &Context,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            None
        } else if how_expanded < 1.0 {
            // 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_width = PanelState::load(ctx, self.id)
                .map_or(self.default_width, |state| state.rect.width());
            let fake_width = how_expanded * expanded_width;
            Self {
                id: self.id.with("animating_panel"),
                ..self
            }
            .resizable(false)
            .exact_width(fake_width)
            .show(ctx, |_ui| {});
            None
        } else {
            // Show the real panel:
            Some(self.show(ctx, add_contents))
        }
    }

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated_inside<R>(
        self,
        ui: &mut Ui,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            None
        } else if how_expanded < 1.0 {
            // 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_width = PanelState::load(ui.ctx(), self.id)
                .map_or(self.default_width, |state| state.rect.width());
            let fake_width = how_expanded * expanded_width;
            Self {
                id: self.id.with("animating_panel"),
                ..self
            }
            .resizable(false)
            .exact_width(fake_width)
            .show_inside(ui, |_ui| {});
            None
        } else {
            // Show the real panel:
            Some(self.show_inside(ui, add_contents))
        }
    }

    /// Show either a collapsed or a expanded panel, with a nice animation between.
    fn show_animated_between<R>(
        ctx: &Context,
        is_expanded: bool,
        collapsed_panel: Self,
        expanded_panel: Self,
        add_contents: impl FnOnce(&mut Ui, f32) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
        } else if how_expanded < 1.0 {
            // Show animation:
            let collapsed_width = PanelState::load(ctx, collapsed_panel.id)
                .map_or(collapsed_panel.default_width, |state| state.rect.width());
            let expanded_width = PanelState::load(ctx, expanded_panel.id)
                .map_or(expanded_panel.default_width, |state| state.rect.width());
            let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
            Self {
                id: expanded_panel.id.with("animating_panel"),
                ..expanded_panel
            }
            .resizable(false)
            .exact_width(fake_width)
            .show(ctx, |ui| add_contents(ui, how_expanded));
            None
        } else {
            Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
        }
    }

    /// Show either a collapsed or a expanded panel, with a nice animation between.
    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 how_expanded =
            animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
        } else if how_expanded < 1.0 {
            // Show animation:
            let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id)
                .map_or(collapsed_panel.default_width, |state| state.rect.width());
            let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id)
                .map_or(expanded_panel.default_width, |state| state.rect.width());
            let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
            Self {
                id: expanded_panel.id.with("animating_panel"),
                ..expanded_panel
            }
            .resizable(false)
            .exact_width(fake_width)
            .show_inside(ui, |ui| add_contents(ui, how_expanded))
        } else {
            expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
        }
    }
}

impl VerticalPanel {
    /// Show the panel inside a [`Ui`].
    fn show_inside_dyn<'c, R>(
        self,
        ui: &mut Ui,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> InnerResponse<R> {
        let Self {
            side,
            id,
            frame,
            resizable,
            show_separator_line,
            default_width,
            width_range,
        } = self;

        let available_rect = ui.available_rect_before_wrap();
        let mut panel_rect = available_rect;
        let mut width = default_width;
        {
            if let Some(state) = PanelState::load(ui.ctx(), id) {
                width = state.rect.width();
            }
            width = clamp_to_range(width, width_range).at_most(available_rect.width());
            side.set_rect_width(&mut panel_rect, width);
            ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
        }

        let resize_id = id.with("__resize");
        let mut resize_hover = false;
        let mut is_resizing = false;
        if resizable {
            // First we read the resize interaction results, to avoid frame latency in the resize:
            if let Some(resize_response) = ui.ctx().read_response(resize_id) {
                resize_hover = resize_response.hovered();
                is_resizing = resize_response.dragged();

                if is_resizing {
                    if let Some(pointer) = resize_response.interact_pointer_pos() {
                        width = (pointer.x - side.side_x(panel_rect)).abs();
                        width = clamp_to_range(width, width_range).at_most(available_rect.width());
                        side.set_rect_width(&mut panel_rect, width);
                    }
                }
            }
        }

        let mut panel_ui = ui.new_child(
            UiBuilder::new()
                .id_salt(id)
                .ui_stack_info(UiStackInfo::new(match side {
                    VerticalSide::Left => UiKind::LeftPanel,
                    VerticalSide::Right => UiKind::RightPanel,
                }))
                .max_rect(panel_rect)
                .layout(Layout::top_down(Align::Min)),
        );
        panel_ui.expand_to_include_rect(panel_rect);
        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)

        let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
        let inner_response = frame.show(&mut panel_ui, |ui| {
            ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
            ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0));
            add_contents(ui)
        });

        let rect = inner_response.response.rect;

        {
            let mut cursor = ui.cursor();
            match side {
                VerticalSide::Left => {
                    cursor.min.x = rect.max.x;
                }
                VerticalSide::Right => {
                    cursor.max.x = rect.min.x;
                }
            }
            ui.set_cursor(cursor);
        }
        ui.expand_to_include_rect(rect);

        if resizable {
            // 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
            // `ScrollArea` on either side of the panel boundary.
            let resize_x = side.opposite().side_x(panel_rect);
            let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range())
                .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0));
            let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
            resize_hover = resize_response.hovered();
            is_resizing = resize_response.dragged();
        }

        if resize_hover || is_resizing {
            let cursor_icon = if width <= width_range.min {
                match self.side {
                    VerticalSide::Left => CursorIcon::ResizeEast,
                    VerticalSide::Right => CursorIcon::ResizeWest,
                }
            } else if width < width_range.max {
                CursorIcon::ResizeHorizontal
            } else {
                match self.side {
                    VerticalSide::Left => CursorIcon::ResizeWest,
                    VerticalSide::Right => CursorIcon::ResizeEast,
                }
            };
            ui.ctx().set_cursor_icon(cursor_icon);
        }

        PanelState { rect }.store(ui.ctx(), id);

        {
            let stroke = if is_resizing {
                ui.style().visuals.widgets.active.fg_stroke // highly visible
            } else if resize_hover {
                ui.style().visuals.widgets.hovered.fg_stroke // highly visible
            } else if show_separator_line {
                // TODO(emilk): distinguish resizable from non-resizable
                ui.style().visuals.widgets.noninteractive.bg_stroke // dim
            } else {
                Stroke::NONE
            };
            // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
            let resize_x = side.opposite().side_x(rect);

            // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc)
            let resize_x = ui.painter().round_to_pixel_center(resize_x);

            // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for
            // left-side panels
            let resize_x = resize_x - if side == VerticalSide::Left { 1.0 } else { 0.0 };
            ui.painter().vline(resize_x, panel_rect.y_range(), stroke);
        }

        inner_response
    }

    /// Show the panel at the top level.
    fn show_dyn<'c, R>(
        self,
        ctx: &Context,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> InnerResponse<R> {
        let side = self.side;
        let available_rect = ctx.available_rect();
        let mut panel_ui = Ui::new(
            ctx.clone(),
            self.id,
            UiBuilder::new()
                .layer_id(LayerId::background())
                .max_rect(available_rect),
        );
        panel_ui.set_clip_rect(ctx.screen_rect());

        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
        let rect = inner_response.response.rect;

        match side {
            VerticalSide::Left => ctx.pass_state_mut(|state| {
                state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
            }),
            VerticalSide::Right => ctx.pass_state_mut(|state| {
                state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
            }),
        }
        inner_response
    }
}

// ----------------------------------------------------------------------------

/// [`Top`](HorizontalSide::Top) or [`Bottom`](HorizontalSide::Bottom)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HorizontalSide {
    Top,
    Bottom,
}

impl Side for HorizontalSide {
    fn opposite(self) -> Self {
        match self {
            Self::Top => Self::Bottom,
            Self::Bottom => Self::Top,
        }
    }
}

impl HorizontalSide {
    fn set_rect_height(self, rect: &mut Rect, height: f32) {
        match self {
            Self::Top => rect.max.y = rect.min.y + height,
            Self::Bottom => rect.min.y = rect.max.y - height,
        }
    }

    fn side_y(self, rect: Rect) -> f32 {
        match self {
            Self::Top => rect.top(),
            Self::Bottom => rect.bottom(),
        }
    }
}

/// A panel that covers the entire top or bottom of a [`Ui`] or screen.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ Always add any [`CentralPanel`] last.
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
///    ui.label("Hello World!");
/// });
/// # });
/// ```
///
/// See also [`VerticalPanel`].
#[must_use = "You should call .show()"]
pub struct HorizontalPanel {
    side: HorizontalSide,
    id: Id,
    frame: Option<Frame>,
    resizable: bool,
    show_separator_line: bool,
    default_height: Option<f32>,
    height_range: Rangef,
}

impl PanelOptions for HorizontalPanel {
    fn new(side: HorizontalSide, id: impl Into<Id>) -> Self {
        Self {
            side,
            id: id.into(),
            frame: None,
            resizable: false,
            show_separator_line: true,
            default_height: None,
            height_range: Rangef::new(20.0, f32::INFINITY),
        }
    }

    #[inline]
    fn resizable(mut self, resizable: bool) -> Self {
        self.resizable = resizable;
        self
    }

    #[inline]
    fn show_separator_line(mut self, show_separator_line: bool) -> Self {
        self.show_separator_line = show_separator_line;
        self
    }

    #[inline]
    fn frame(mut self, frame: Frame) -> Self {
        self.frame = Some(frame);
        self
    }
}

impl PanelSize for HorizontalPanel {
    fn default_size(mut self, default_size: f32) -> Self {
        self.default_height(default_size)
    }

    fn min_size(mut self, min_size: f32) -> Self {
        self.min_height(min_size)
    }

    fn max_size(mut self, max_size: f32) -> Self {
        self.max_height(max_size)
    }

    fn size_range(mut self, size_range: impl Into<Rangef>) -> Self {
        self.height_range(size_range)
    }

    fn exact_size(mut self, size: f32) -> Self {
        self.exact_height(size)
    }
}

impl HorizontalPanel {
    /// The id should be globally unique, e.g. `Id::new("my_top_panel")`.
    pub fn top(id: impl Into<Id>) -> Self {
        Self::new(HorizontalSide::Top, id)
    }

    /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`.
    pub fn bottom(id: impl Into<Id>) -> Self {
        Self::new(HorizontalSide::Bottom, id)
    }

    /// The initial height of the [`HorizontalPanel`], including margins.
    /// Defaults to [`crate::style::Spacing::interact_size`].y, plus frame margins.
    #[inline]
    pub fn default_height(mut self, default_height: f32) -> Self {
        self.default_height = Some(default_height);
        self.height_range = Rangef::new(
            self.height_range.min.at_most(default_height),
            self.height_range.max.at_least(default_height),
        );
        self
    }

    /// Minimum height of the panel, including margins.
    #[inline]
    pub fn min_height(mut self, min_height: f32) -> Self {
        self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
        self
    }

    /// Maximum height of the panel, including margins.
    #[inline]
    pub fn max_height(mut self, max_height: f32) -> Self {
        self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
        self
    }

    /// The allowable height range for the panel, including margins.
    #[inline]
    pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
        let height_range = height_range.into();
        self.default_height = self
            .default_height
            .map(|default_height| clamp_to_range(default_height, height_range));
        self.height_range = height_range;
        self
    }

    /// Enforce this exact height, including margins.
    #[inline]
    pub fn exact_height(mut self, height: f32) -> Self {
        self.default_height = Some(height);
        self.height_range = Rangef::point(height);
        self
    }
}

impl PanelActions for HorizontalPanel {
    /// Show the panel inside a [`Ui`].
    fn show_inside<R>(
        self,
        ui: &mut Ui,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R> {
        self.show_inside_dyn(ui, Box::new(add_contents))
    }

    /// Show the panel at the top level.
    fn show<R>(self, ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
        self.show_dyn(ctx, Box::new(add_contents))
    }

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated<R>(
        self,
        ctx: &Context,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            None
        } else if how_expanded < 1.0 {
            // Show a fake panel in this in-between animation state:
            // TODO(emilk): move the panel out-of-screen instead of changing its height.
            // Then we can actually paint it as it animates.
            let expanded_height = PanelState::load(ctx, self.id)
                .map(|state| state.rect.height())
                .or(self.default_height)
                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
            let fake_height = how_expanded * expanded_height;
            Self {
                id: self.id.with("animating_panel"),
                ..self
            }
            .resizable(false)
            .exact_height(fake_height)
            .show(ctx, |_ui| {});
            None
        } else {
            // Show the real panel:
            Some(self.show(ctx, add_contents))
        }
    }

    /// Show the panel if `is_expanded` is `true`,
    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
    fn show_animated_inside<R>(
        self,
        ui: &mut Ui,
        is_expanded: bool,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            None
        } else if how_expanded < 1.0 {
            // Show a fake panel in this in-between animation state:
            // TODO(emilk): move the panel out-of-screen instead of changing its height.
            // Then we can actually paint it as it animates.
            let expanded_height = PanelState::load(ui.ctx(), self.id)
                .map(|state| state.rect.height())
                .or(self.default_height)
                .unwrap_or_else(|| ui.style().spacing.interact_size.y);
            let fake_height = how_expanded * expanded_height;
            Self {
                id: self.id.with("animating_panel"),
                ..self
            }
            .resizable(false)
            .exact_height(fake_height)
            .show_inside(ui, |_ui| {});
            None
        } else {
            // Show the real panel:
            Some(self.show_inside(ui, add_contents))
        }
    }

    /// Show either a collapsed or a expanded panel, with a nice animation between.
    fn show_animated_between<R>(
        ctx: &Context,
        is_expanded: bool,
        collapsed_panel: Self,
        expanded_panel: Self,
        add_contents: impl FnOnce(&mut Ui, f32) -> R,
    ) -> Option<InnerResponse<R>> {
        let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
        } else if how_expanded < 1.0 {
            // Show animation:
            let collapsed_height = PanelState::load(ctx, collapsed_panel.id)
                .map(|state| state.rect.height())
                .or(collapsed_panel.default_height)
                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);

            let expanded_height = PanelState::load(ctx, expanded_panel.id)
                .map(|state| state.rect.height())
                .or(expanded_panel.default_height)
                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);

            let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
            Self {
                id: expanded_panel.id.with("animating_panel"),
                ..expanded_panel
            }
            .resizable(false)
            .exact_height(fake_height)
            .show(ctx, |ui| add_contents(ui, how_expanded));
            None
        } else {
            Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
        }
    }

    /// Show either a collapsed or a expanded panel, with a nice animation between.
    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 how_expanded =
            animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);

        if 0.0 == how_expanded {
            collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
        } else if how_expanded < 1.0 {
            // Show animation:
            let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id)
                .map(|state| state.rect.height())
                .or(collapsed_panel.default_height)
                .unwrap_or_else(|| ui.style().spacing.interact_size.y);

            let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id)
                .map(|state| state.rect.height())
                .or(expanded_panel.default_height)
                .unwrap_or_else(|| ui.style().spacing.interact_size.y);

            let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
            Self {
                id: expanded_panel.id.with("animating_panel"),
                ..expanded_panel
            }
            .resizable(false)
            .exact_height(fake_height)
            .show_inside(ui, |ui| add_contents(ui, how_expanded))
        } else {
            expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
        }
    }
}

impl HorizontalPanel {
    /// Show the panel inside a [`Ui`].
    fn show_inside_dyn<'c, R>(
        self,
        ui: &mut Ui,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> InnerResponse<R> {
        let Self {
            side,
            id,
            frame,
            resizable,
            show_separator_line,
            default_height,
            height_range,
        } = self;

        let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));

        let available_rect = ui.available_rect_before_wrap();
        let mut panel_rect = available_rect;

        let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
            state.rect.height()
        } else {
            default_height
                .unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y)
        };
        {
            height = clamp_to_range(height, height_range).at_most(available_rect.height());
            side.set_rect_height(&mut panel_rect, height);
            ui.ctx()
                .check_for_id_clash(id, panel_rect, "TopBottomPanel");
        }

        let resize_id = id.with("__resize");
        let mut resize_hover = false;
        let mut is_resizing = false;
        if resizable {
            // First we read the resize interaction results, to avoid frame latency in the resize:
            if let Some(resize_response) = ui.ctx().read_response(resize_id) {
                resize_hover = resize_response.hovered();
                is_resizing = resize_response.dragged();

                if is_resizing {
                    if let Some(pointer) = resize_response.interact_pointer_pos() {
                        height = (pointer.y - side.side_y(panel_rect)).abs();
                        height =
                            clamp_to_range(height, height_range).at_most(available_rect.height());
                        side.set_rect_height(&mut panel_rect, height);
                    }
                }
            }
        }

        let mut panel_ui = ui.new_child(
            UiBuilder::new()
                .id_salt(id)
                .ui_stack_info(UiStackInfo::new(match side {
                    HorizontalSide::Top => UiKind::TopPanel,
                    HorizontalSide::Bottom => UiKind::BottomPanel,
                }))
                .max_rect(panel_rect)
                .layout(Layout::top_down(Align::Min)),
        );
        panel_ui.expand_to_include_rect(panel_rect);
        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)

        let inner_response = frame.show(&mut panel_ui, |ui| {
            ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
            ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0));
            add_contents(ui)
        });

        let rect = inner_response.response.rect;

        {
            let mut cursor = ui.cursor();
            match side {
                HorizontalSide::Top => {
                    cursor.min.y = rect.max.y;
                }
                HorizontalSide::Bottom => {
                    cursor.max.y = rect.min.y;
                }
            }
            ui.set_cursor(cursor);
        }
        ui.expand_to_include_rect(rect);

        if resizable {
            // 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
            // `ScrollArea` on either side of the panel boundary.

            let resize_y = side.opposite().side_y(panel_rect);
            let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y)
                .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));
            let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
            resize_hover = resize_response.hovered();
            is_resizing = resize_response.dragged();
        }

        if resize_hover || is_resizing {
            let cursor_icon = if height <= height_range.min {
                match self.side {
                    HorizontalSide::Top => CursorIcon::ResizeSouth,
                    HorizontalSide::Bottom => CursorIcon::ResizeNorth,
                }
            } else if height < height_range.max {
                CursorIcon::ResizeVertical
            } else {
                match self.side {
                    HorizontalSide::Top => CursorIcon::ResizeNorth,
                    HorizontalSide::Bottom => CursorIcon::ResizeSouth,
                }
            };
            ui.ctx().set_cursor_icon(cursor_icon);
        }

        PanelState { rect }.store(ui.ctx(), id);

        {
            let stroke = if is_resizing {
                ui.style().visuals.widgets.active.fg_stroke // highly visible
            } else if resize_hover {
                ui.style().visuals.widgets.hovered.fg_stroke // highly visible
            } else if show_separator_line {
                // TODO(emilk): distinguish resizable from non-resizable
                ui.style().visuals.widgets.noninteractive.bg_stroke // dim
            } else {
                Stroke::NONE
            };
            // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
            let resize_y = side.opposite().side_y(rect);

            // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc)
            let resize_y = ui.painter().round_to_pixel_center(resize_y);

            // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for
            // top-side panels
            let resize_y = resize_y
                - if side == HorizontalSide::Top {
                    1.0
                } else {
                    0.0
                };
            ui.painter().hline(panel_rect.x_range(), resize_y, stroke);
        }

        inner_response
    }

    /// Show the panel at the top level.
    fn show_dyn<'c, R>(
        self,
        ctx: &Context,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> InnerResponse<R> {
        let available_rect = ctx.available_rect();
        let side = self.side;

        let mut panel_ui = Ui::new(
            ctx.clone(),
            self.id,
            UiBuilder::new()
                .layer_id(LayerId::background())
                .max_rect(available_rect),
        );
        panel_ui.set_clip_rect(ctx.screen_rect());

        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
        let rect = inner_response.response.rect;

        match side {
            HorizontalSide::Top => {
                ctx.pass_state_mut(|state| {
                    state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
                });
            }
            HorizontalSide::Bottom => {
                ctx.pass_state_mut(|state| {
                    state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
                });
            }
        }

        inner_response
    }
}

// ----------------------------------------------------------------------------

/// A panel that covers the remainder of the screen,
/// i.e. whatever area is left after adding other panels.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ [`CentralPanel`] must be added after all other panels!
///
/// NOTE: Any [`crate::Window`]s and [`crate::Area`]s will cover the top-level [`CentralPanel`].
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
///    ui.label("Hello World! From `TopBottomPanel`, that must be before `CentralPanel`!");
/// });
/// egui::CentralPanel::default().show(ctx, |ui| {
///    ui.label("Hello World!");
/// });
/// # });
/// ```
#[must_use = "You should call .show()"]
#[derive(Default)]
pub struct CentralPanel {
    frame: Option<Frame>,
}

impl CentralPanel {
    /// Change the background color, margins, etc.
    #[inline]
    pub fn frame(mut self, frame: Frame) -> Self {
        self.frame = Some(frame);
        self
    }
}

impl CentralPanel {
    /// Show the panel inside a [`Ui`].
    pub fn show_inside<R>(
        self,
        ui: &mut Ui,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R> {
        self.show_inside_dyn(ui, Box::new(add_contents))
    }

    /// Show the panel inside a [`Ui`].
    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 panel_rect = ui.available_rect_before_wrap();
        let mut panel_ui = ui.new_child(
            UiBuilder::new()
                .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
                .max_rect(panel_rect)
                .layout(Layout::top_down(Align::Min)),
        );
        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)

        let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
        frame.show(&mut panel_ui, |ui| {
            ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all
            add_contents(ui)
        })
    }

    /// Show the panel at the top level.
    pub fn show<R>(
        self,
        ctx: &Context,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R> {
        self.show_dyn(ctx, Box::new(add_contents))
    }

    /// Show the panel at the top level.
    fn show_dyn<'c, R>(
        self,
        ctx: &Context,
        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
    ) -> InnerResponse<R> {
        let available_rect = ctx.available_rect();
        let id = Id::new((ctx.viewport_id(), "central_panel"));

        let mut panel_ui = Ui::new(
            ctx.clone(),
            id,
            UiBuilder::new()
                .layer_id(LayerId::background())
                .max_rect(available_rect),
        );
        panel_ui.set_clip_rect(ctx.screen_rect());

        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);

        // Only inform ctx about what we actually used, so we can shrink the native window to fit.
        ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));

        inner_response
    }
}

fn clamp_to_range(x: f32, range: Rangef) -> f32 {
    let range = range.as_positive();
    x.clamp(range.min, range.max)
}

Describe alternatives you've considered

Having a wrapper that duplicate the functions for either TopBottom and Side, going a bit against the goal I wanted.

Additional context

I'm really new to Rust, for forgive me if my proposal is a Rustaceans crime 😆.

@sharky98
Copy link
Author

I'll try to gather a PR to show how it could look like 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant