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

feature: programmatically hide/show or expand/collapse windows #4835

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions crates/egui/src/containers/collapsing_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ use std::hash::Hash;

use crate::*;
use epaint::Shape;
use window::WindowAction;

#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct InnerState {
open: bool,
expanded: bool,
shown: bool,

/// Height of the region when open. Used for animations
#[cfg_attr(feature = "serde", serde(default))]
Expand Down Expand Up @@ -48,22 +50,51 @@ impl CollapsingState {
Self::load(ctx, id).unwrap_or(Self {
id,
state: InnerState {
open: default_open,
expanded: default_open,
shown: true,
open_height: None,
},
})
}

#[deprecated = "Renamed to `is_expanded` function"]
pub fn is_open(&self) -> bool {
self.state.open
self.state.expanded
}

pub fn set_open(&mut self, open: bool) {
self.state.open = open;
pub fn set_action(&mut self, action: &WindowAction) {
match action {
WindowAction::Show => self.state.shown = true,
WindowAction::Hide => self.state.shown = false,
WindowAction::Expand => self.state.expanded = true,
WindowAction::Collapse => self.state.expanded = false,
WindowAction::ToggleShow => {
self.state.shown = !self.state.shown;
}
WindowAction::ToggleExpand => {
self.state.expanded = !self.state.expanded;
}
}
}

pub fn set_expanded(&mut self, expanded: bool) {
self.state.expanded = expanded;
}

pub fn is_expanded(&self) -> bool {
self.state.expanded
}

pub fn set_shown(&mut self, shown: bool) {
self.state.shown = shown;
}

pub fn is_shown(&mut self) -> bool {
self.state.shown
}

pub fn toggle(&mut self, ui: &Ui) {
self.state.open = !self.state.open;
self.state.expanded = !self.state.expanded;
ui.ctx().request_repaint();
}

Expand All @@ -72,7 +103,7 @@ impl CollapsingState {
if ctx.memory(|mem| mem.everything_is_visible()) {
1.0
} else {
ctx.animate_bool_responsive(self.id, self.state.open)
ctx.animate_bool_responsive(self.id, self.state.expanded)
}
}

Expand Down Expand Up @@ -196,7 +227,7 @@ impl CollapsingState {
None
} else if openness < 1.0 {
Some(ui.scope(|child_ui| {
let max_height = if self.state.open && self.state.open_height.is_none() {
let max_height = if self.state.expanded && self.state.open_height.is_none() {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placeholder value that shows some movement:
Expand Down Expand Up @@ -272,12 +303,17 @@ pub struct HeaderResponse<'ui, HeaderRet> {
}

impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
#[deprecated = "Use the `HeaderResponse::is_expanded` instead"]
pub fn is_open(&self) -> bool {
self.state.is_open()
self.is_expanded()
}

pub fn is_expanded(&self) -> bool {
self.state.is_expanded()
}

pub fn set_open(&mut self, open: bool) {
self.state.set_open(open);
self.state.set_expanded(open);
}

pub fn toggle(&mut self) {
Expand Down Expand Up @@ -530,8 +566,8 @@ impl CollapsingHeader {
);

let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
if let Some(open) = open {
if open != state.is_open() {
if let Some(is_shown) = open {
if is_shown != state.is_shown() {
state.toggle(ui);
header_response.mark_changed();
}
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ pub use {
popup::*,
resize::Resize,
scroll_area::ScrollArea,
window::Window,
window::{Window, WindowAction},
};
113 changes: 79 additions & 34 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ use super::*;
#[must_use = "You should call .show()"]
pub struct Window<'open> {
title: WidgetText,
window_action: Option<WindowAction>,
open: Option<&'open mut bool>,
show_close_button: bool,
area: Area,
frame: Option<Frame>,
resize: Resize,
Expand All @@ -43,6 +45,20 @@ pub struct Window<'open> {
fade_out: bool,
}

#[derive(Debug, Clone, Copy)]
pub enum WindowAction {
Expand,
Collapse,
Show,
Hide,

/// Toggles between show and hide
ToggleShow,

/// Toggles between expand and collapse
ToggleExpand,
}

impl<'open> Window<'open> {
/// The window title is used as a unique [`Id`] and must be unique, and should not change.
/// This is true even if you disable the title bar with `.title_bar(false)`.
Expand All @@ -52,9 +68,10 @@ impl<'open> Window<'open> {
let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
Self {
title,
open: None,
area,
frame: None,
window_action: None,
open: None,
resize: Resize::default()
.with_stroke(false)
.min_size([96.0, 32.0])
Expand All @@ -64,6 +81,7 @@ impl<'open> Window<'open> {
default_open: true,
with_title_bar: true,
fade_out: true,
show_close_button: false,
}
}

Expand All @@ -74,11 +92,29 @@ impl<'open> Window<'open> {
self
}

/// Call this to add a close-button to the window title bar.
///
/// * If `*open == false`, the window will not be visible.
/// * If `*open == true`, the window will have a close button.
/// Call this to programmatically expand/collapse or to show/hide the window.
/// The passed Option value will be taken, so the option will be set to None.
#[inline]
pub fn window_action(mut self, action: &mut Option<WindowAction>) -> Self {
self.window_action = action.take();
self
}

/// Set if you want to have a close/hide button on the window
#[inline]
pub fn with_close_button(mut self, value: bool) -> Self {
self.show_close_button = value;
self
}

/// Call this to add a close-button to the window title bar.

///
/// * If `*open == false`, the window will not be visible.
/// * If `*open == true`, the window will have a close button.
/// * If the close button is pressed, `*open` will be set to `false`.
///
/// You can also use the none borrowing `Window::window_action` function instead.
#[inline]
pub fn open(mut self, open: &'open mut bool) -> Self {
self.open = Some(open);
Expand Down Expand Up @@ -420,6 +456,7 @@ impl<'open> Window<'open> {
) -> Option<InnerResponse<Option<R>>> {
let Window {
title,
window_action,
open,
area,
frame,
Expand All @@ -429,6 +466,7 @@ impl<'open> Window<'open> {
default_open,
with_title_bar,
fade_out,
show_close_button,
} = self;

let header_color =
Expand All @@ -440,25 +478,33 @@ impl<'open> Window<'open> {
// Add border padding to the inner margin to prevent it from covering the contents
window_frame.inner_margin += border_padding;

let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
let all_visible = ctx.memory(|mem| mem.everything_is_visible());
let area_id = area.id;

let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);

if let Some(action) = &window_action {
collapsing.set_action(action);
} else if let Some(open) = open.as_ref() {
collapsing.set_shown(**open);
}

let opacity = ctx.animate_bool_with_easing(
area.id.with("fade-out"),
is_open,
collapsing.is_shown() || all_visible,
emath::easing::cubic_out,
);

if opacity <= 0.0 {
return None;
}

let area_id = area.id;
let area_layer_id = area.layer();
let resize_id = area_id.with("resize");
let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);

let is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
let is_expanded = with_title_bar && collapsing.is_expanded();
let possible = PossibleInteractions::new(&area, &resize, is_expanded);

let resize = resize.resizable(false); // We resize it manually
let mut resize = resize.id(resize_id);
Expand Down Expand Up @@ -506,10 +552,10 @@ impl<'open> Window<'open> {
);

let mut area_content_ui = area.content_ui(ctx);
if is_open {
// `Area` already takes care of fade-in animations,
// so we only need to handle fade-out animations here.
} else if fade_out {

// `Area` already takes care of fade-in animations,
// so we only need to handle fade-out animations here.
if !collapsing.is_expanded() && fade_out {
area_content_ui.multiply_opacity(opacity);
}

Expand All @@ -518,8 +564,6 @@ impl<'open> Window<'open> {
let frame_stroke = window_frame.stroke;
let mut frame = window_frame.begin(&mut area_content_ui);

let show_close_button = open.is_some();

let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);

// Backup item spacing before the title bar
Expand Down Expand Up @@ -560,6 +604,7 @@ impl<'open> Window<'open> {
.map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));

let outer_rect = frame.end(&mut area_content_ui).rect;

paint_resize_corner(
&area_content_ui,
&possible,
Expand Down Expand Up @@ -587,7 +632,7 @@ impl<'open> Window<'open> {
// Eliminate the rounding gap between the title bar and the window frame
round -= border_padding;

if !is_collapsed {
if is_expanded {
round.se = 0.0;
round.sw = 0.0;
}
Expand All @@ -607,9 +652,9 @@ impl<'open> Window<'open> {
&mut area_content_ui,
title_rect,
&content_response,
open,
&mut collapsing,
collapsible,
show_close_button,
);
}

Expand All @@ -622,11 +667,15 @@ impl<'open> Window<'open> {

let full_response = area.end(ctx, area_content_ui);

let inner_response = InnerResponse {
// Resync if open & window_action are used together.
if let Some(open) = open {
*open = collapsing.is_shown();
}

Some(InnerResponse {
inner: content_inner,
response: full_response,
};
Some(inner_response)
})
}
}

Expand Down Expand Up @@ -691,11 +740,9 @@ struct PossibleInteractions {
}

impl PossibleInteractions {
fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
fn new(area: &Area, resize: &Resize, is_expanded: bool) -> Self {
let movable = area.is_enabled() && area.is_movable();
let resizable = resize
.is_resizable()
.and(area.is_enabled() && !is_collapsed);
let resizable = resize.is_resizable().and(area.is_enabled() && is_expanded);
let pivot = area.get_pivot();
Self {
resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
Expand Down Expand Up @@ -1113,20 +1160,18 @@ impl TitleBar {
ui: &mut Ui,
outer_rect: Rect,
content_response: &Option<Response>,
open: Option<&mut bool>,
collapsing: &mut CollapsingState,
collapsible: bool,
show_closing_btn: bool,
) {
if let Some(content_response) = &content_response {
// Now we know how large we got to be:
self.rect.max.x = self.rect.max.x.max(content_response.rect.max.x);
}

if let Some(open) = open {
// Add close button now that we know our full width:
if self.close_button_ui(ui).clicked() {
*open = false;
}
// Add close button now that we know our full width:
if show_closing_btn && self.close_button_ui(ui).clicked() {
collapsing.set_action(&WindowAction::Hide);
}

let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
Expand Down
Loading
Loading