diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 5d12dc79432..e3a1c1266a9 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -6,7 +6,8 @@ use epaint::Shape; #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct InnerState { - open: bool, + open: bool, // Expand / collapse + hidden: bool, // Show / Hide /// Height of the region when open. Used for animations #[cfg_attr(feature = "serde", serde(default))] @@ -25,13 +26,6 @@ pub struct CollapsingState { } impl CollapsingState { - pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|d| { - d.get_persisted::(id) - .map(|state| Self { id, state }) - }) - } - pub fn store(&self, ctx: &Context) { ctx.data_mut(|d| d.insert_persisted(self.id, self.state)); } @@ -44,11 +38,16 @@ impl CollapsingState { self.id } - pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self { - Self::load(ctx, id).unwrap_or(CollapsingState { + pub fn load(ctx: &Context, id: Id, default_open: bool) -> Self { + ctx.data_mut(|d| { + d.get_persisted::(id) + .map(|state| Self { id, state }) + }) + .unwrap_or(CollapsingState { id, state: InnerState { open: default_open, + hidden: false, open_height: None, }, }) @@ -62,11 +61,23 @@ impl CollapsingState { self.state.open = open; } - pub fn toggle(&mut self, ui: &Ui) { + pub fn toggle_open(&mut self, ui: &Ui) { self.state.open = !self.state.open; ui.ctx().request_repaint(); } + pub fn is_hidden(&self) -> bool { + self.state.hidden + } + + pub fn toggle_hidden(&mut self) { + self.state.hidden = !self.state.hidden; + } + + pub fn set_hidden(&mut self, hidden: bool) { + self.state.hidden = hidden; + } + /// 0 for closed, 1 for open, with tweening pub fn openness(&self, ctx: &Context) -> f32 { if ctx.memory(|mem| mem.everything_is_visible()) { @@ -85,7 +96,7 @@ impl CollapsingState { let (_id, rect) = ui.allocate_space(button_size); let response = ui.interact(rect, self.id, Sense::click()); if response.clicked() { - self.toggle(ui); + self.toggle_open(ui); } let openness = self.openness(ui.ctx()); paint_default_icon(ui, openness, &response); @@ -107,7 +118,7 @@ impl CollapsingState { let (_id, rect) = ui.allocate_space(size); let response = ui.interact(rect, self.id, Sense::click()); if response.clicked() { - self.toggle(ui); + self.toggle_open(ui); } let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect); @@ -535,14 +546,15 @@ impl CollapsingHeader { header_response.rect.center().y - text.size().y / 2.0, ); - let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open); + let mut state = CollapsingState::load(ui.ctx(), id, default_open); + if let Some(open) = open { if open != state.is_open() { - state.toggle(ui); + state.toggle_open(ui); header_response.mark_changed(); } } else if header_response.clicked() { - state.toggle(ui); + state.toggle_open(ui); header_response.mark_changed(); } diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 53e8e7e2ce0..95ee9fbd896 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -21,5 +21,5 @@ pub use { popup::*, resize::Resize, scroll_area::ScrollArea, - window::Window, + window::{DisplayEvent, Window}, }; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c12199d40ac..371860c52ef 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -27,7 +27,6 @@ use super::*; #[must_use = "You should call .show()"] pub struct Window<'open> { title: WidgetText, - open: Option<&'open mut bool>, area: Area, frame: Option, resize: Resize, @@ -35,6 +34,26 @@ pub struct Window<'open> { collapsible: bool, default_open: bool, with_title_bar: bool, + with_closing_btn: bool, + display_event: Option, + open: Option<&'open mut bool>, +} + +#[derive(PartialEq, Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum DisplayEvent { + #[default] + Expand, + Collapse, + Hide, + ToggleCollapse, + ToggleHidden, +} + +impl DisplayEvent { + pub fn equals(&self, other: DisplayEvent) -> bool { + self == &other + } } impl<'open> Window<'open> { @@ -44,6 +63,7 @@ impl<'open> Window<'open> { pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); let area = Area::new(Id::new(title.text())).constrain(true); + Self { title, open: None, @@ -55,8 +75,10 @@ impl<'open> Window<'open> { .default_size([340.0, 420.0]), // Default inner size of a window scroll: ScrollArea::neither(), collapsible: true, - default_open: true, with_title_bar: true, + with_closing_btn: false, + default_open: true, + display_event: None, } } @@ -71,8 +93,15 @@ impl<'open> Window<'open> { /// * 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`. + //#[deprecated = "Use display_state and closing_button instead"] pub fn open(mut self, open: &'open mut bool) -> Self { self.open = Some(open); + self.with_closing_btn = true; + self + } + + pub fn closing_button(mut self, closing_button: bool) -> Self { + self.with_closing_btn = closing_button; self } @@ -205,6 +234,11 @@ impl<'open> Window<'open> { self } + pub fn display_event(mut self, new_event: &mut Option) -> Self { + self.display_event = new_event.take(); + self + } + /// Set initial size of the window. pub fn default_size(mut self, default_size: impl Into) -> Self { self.resize = self.resize.default_size(default_size); @@ -317,33 +351,58 @@ impl<'open> Window<'open> { ) -> Option>> { let Window { title, - open, + mut open, area, frame, resize, scroll, collapsible, - default_open, with_title_bar, + with_closing_btn, + default_open, + display_event, } = self; let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - let is_explicitly_closed = matches!(open, Some(false)); - let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); + let area_id = area.id; + let mut collapsing = CollapsingState::load(ctx, area_id.with("collapsing"), default_open); + + // Borrow open to not consume it. + // This is for backwards compatibility with events + fn backwards_compatibility_open( + open: &mut Option<&mut bool>, + collapsing: &CollapsingState, + ) { + if let Some(open) = open { + **open = !collapsing.is_hidden(); + } + } + + match display_event { + Some(DisplayEvent::Hide) => collapsing.set_hidden(true), + Some(DisplayEvent::Expand) => collapsing.set_open(true), + Some(DisplayEvent::Collapse) => collapsing.set_open(false), + Some(DisplayEvent::ToggleHidden) => collapsing.toggle_hidden(), + Some(DisplayEvent::ToggleCollapse) => collapsing.set_open(!collapsing.is_open()), + None => collapsing.set_hidden(matches!(open, Some(false))), + } + + let is_open = !collapsing.is_hidden() || ctx.memory(|mem| mem.everything_is_visible()); area.show_open_close_animation(ctx, &frame, is_open); + backwards_compatibility_open(&mut open, &collapsing); + if !is_open { + collapsing.store(ctx); 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 area_layer_id = area.layer(); let is_collapsed = with_title_bar && !collapsing.is_open(); + let possible = PossibleInteractions::new(&area, &resize, is_collapsed); let area = area.movable(false); // We move it manually, or the area will move the window when we want to resize it @@ -388,6 +447,7 @@ impl<'open> Window<'open> { } else { None }; + let hover_interaction = resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); let mut area_content_ui = area.content_ui(ctx); @@ -397,12 +457,11 @@ impl<'open> Window<'open> { let frame_stroke = frame.stroke; let mut frame = frame.begin(&mut area_content_ui); - let show_close_button = open.is_some(); let title_bar = if with_title_bar { let title_bar = show_title_bar( &mut frame.content_ui, title, - show_close_button, + with_closing_btn, &mut collapsing, collapsible, ); @@ -438,7 +497,7 @@ impl<'open> Window<'open> { &mut area_content_ui, outer_rect, &content_response, - open, + with_closing_btn, &mut collapsing, collapsible, ); @@ -468,11 +527,13 @@ impl<'open> Window<'open> { let full_response = area.end(ctx, area_content_ui); - let inner_response = InnerResponse { + // Hide butten could have been pressed + backwards_compatibility_open(&mut open, &collapsing); + + Some(InnerResponse { inner: content_inner, response: full_response, - }; - Some(inner_response) + }) } } @@ -907,7 +968,7 @@ impl TitleBar { ui: &mut Ui, outer_rect: Rect, content_response: &Option, - open: Option<&mut bool>, + with_closing_btn: bool, collapsing: &mut CollapsingState, collapsible: bool, ) { @@ -916,11 +977,9 @@ impl TitleBar { 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 with_closing_btn && self.close_button_ui(ui).clicked() { + collapsing.toggle_hidden(); } let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range()); @@ -950,7 +1009,7 @@ impl TitleBar { .double_clicked() && collapsible { - collapsing.toggle(ui); + collapsing.toggle_open(ui); } } diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 9ee3dee7d8e..8eb8238b3b1 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -1,7 +1,11 @@ +use egui::DisplayEvent; + #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] -pub struct About {} +pub struct About { + display_event: Option, +} impl super::Demo for About { fn name(&self) -> &'static str { @@ -13,6 +17,7 @@ impl super::Demo for About { .default_width(320.0) .default_height(480.0) .open(open) + .display_event(&mut self.display_event) .show(ctx, |ui| { use super::View as _; self.ui(ui); @@ -20,6 +25,16 @@ impl super::Demo for About { } } +impl About { + pub fn toggle_collapse(&mut self) { + self.display_event = Some(DisplayEvent::ToggleCollapse); + } + + pub fn toggle_hidden(&mut self) { + self.display_event = Some(DisplayEvent::ToggleHidden); + } +} + impl super::View for About { fn ui(&mut self, ui: &mut egui::Ui) { use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS}; diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 0e82d03c23d..ee5de1b3bda 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -268,6 +268,12 @@ impl DemoWindows { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::menu::bar(ui, |ui| { file_menu_button(ui); + if ui.button("Expand / Collapse event").clicked() { + self.about.toggle_collapse(); + } + if ui.button("Show / hide event").clicked() { + self.about.toggle_hidden(); + } }); }); diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index 195b79e223a..64e91c8b8a7 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -401,7 +401,7 @@ impl CustomCollapsingHeader { ui.label("Example of a collapsing header with custom header:"); let id = ui.make_persistent_id("my_collapsing_header"); - egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true) + egui::collapsing_header::CollapsingState::load(ui.ctx(), id, true) .show_header(ui, |ui| { ui.toggle_value(&mut self.selected, "Click to select/unselect"); ui.radio_value(&mut self.radio_value, false, "");