diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 2fc28d3dd51..1e68248af77 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -6,8 +6,12 @@ use epaint::Shape; #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct InnerState { + /// Expand / collapse open: bool, + /// Show / Hide (only used on egui::Window components) + hidden: bool, + /// Height of the region when open. Used for animations #[cfg_attr(feature = "serde", serde(default))] open_height: Option, @@ -25,13 +29,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,16 +41,26 @@ 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, }, }) } + #[deprecated = "use load instead"] + pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self { + Self::load(ctx, id, default_open) + } + pub fn is_open(&self) -> bool { self.state.open } @@ -62,11 +69,26 @@ 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(); } + /// Only used on `egui::Window` components + pub fn is_hidden(&self) -> bool { + self.state.hidden + } + + /// Only used on `egui::Window` components + pub fn toggle_hidden(&mut self) { + self.state.hidden = !self.state.hidden; + } + + /// Only used on `egui::Window` components + 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 +107,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 +129,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); @@ -132,7 +154,7 @@ impl CollapsingState { /// ``` /// # egui::__run_test_ui(|ui| { /// let id = ui.make_persistent_id("my_collapsing_header"); - /// egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false) + /// egui::collapsing_header::CollapsingState::load(ui.ctx(), id, false) /// .show_header(ui, |ui| { /// ui.label("Header"); // you can put checkboxes or whatever here /// }) @@ -239,7 +261,7 @@ impl CollapsingState { /// ui.painter().circle_filled(response.rect.center(), radius, stroke.color); /// } /// - /// let mut state = egui::collapsing_header::CollapsingState::load_with_default_open( + /// let mut state = egui::collapsing_header::CollapsingState::load( /// ui.ctx(), /// ui.make_persistent_id("my_collapsing_state"), /// false, @@ -334,6 +356,16 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) { )); } +/// An event to expand / collapse the widget automatically +#[derive(Eq, PartialEq, Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum CollapsingHeaderEvent { + #[default] + Expand, + Collapse, + ToggleCollapse, +} + /// A function that paints an icon indicating if the region is open or not pub type IconPainter = Box; @@ -357,6 +389,7 @@ pub struct CollapsingHeader { text: WidgetText, default_open: bool, open: Option, + display_event: Option, id_source: Id, enabled: bool, selectable: bool, @@ -385,6 +418,7 @@ impl CollapsingHeader { selected: false, show_background: false, icon: None, + display_event: None, } } @@ -407,6 +441,15 @@ impl CollapsingHeader { self } + /// An event that will change the collapse / hide status of your collapsing header. + /// The difference with this and `.open(...)` is the ability to collapse the header. + /// It also will consume the event and won't borrow the parameter. + #[inline] + pub fn display_event(mut self, new_event: &mut Option) -> Self { + self.display_event = new_event.take(); + self + } + /// Explicitly set the source of the [`Id`] of this widget, instead of using title label. /// This is useful if the title label is dynamic or not unique. #[inline] @@ -514,8 +557,8 @@ impl CollapsingHeader { selectable, selected, show_background, + display_event, } = self; - // TODO(emilk): horizontal layout, with icon and text as labels. Insert background behind using Frame. let id = ui.make_persistent_id(id_source); @@ -543,14 +586,26 @@ 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); - if let Some(open) = open { + let mut state = CollapsingState::load(ui.ctx(), id, default_open); + + let request_repaint = display_event.is_some(); + + match display_event { + Some(CollapsingHeaderEvent::Expand) => state.set_open(true), + Some(CollapsingHeaderEvent::Collapse) => state.set_open(false), + Some(CollapsingHeaderEvent::ToggleCollapse) => state.set_open(!state.is_open()), + None => {} + } + + if request_repaint { + ui.ctx().request_repaint(); + } else 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..b1479e541b3 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::{SetEvent, Window, WindowEvent}, }; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 66bb4a93081..a234b11e50e 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -30,7 +30,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, @@ -38,6 +37,31 @@ 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(Eq, PartialEq, Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum WindowEvent { + #[default] + Expand, + Collapse, + Hide, + ToggleCollapse, + ToggleHidden, +} + +// Helper function for user space +pub trait SetEvent { + fn set(&mut self, display_event: WindowEvent); +} + +impl SetEvent for Option { + fn set(&mut self, display_event: WindowEvent) { + *self = Some(display_event); + } } impl<'open> Window<'open> { @@ -47,6 +71,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, @@ -58,8 +83,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, } } @@ -78,6 +105,13 @@ impl<'open> Window<'open> { #[inline] pub fn open(mut self, open: &'open mut bool) -> Self { self.open = Some(open); + self.with_closing_btn = true; + self + } + + #[inline] + pub fn closing_button(mut self, closing_button: bool) -> Self { + self.with_closing_btn = closing_button; self } @@ -255,6 +289,15 @@ impl<'open> Window<'open> { self } + /// An event that will change the collapse / hide status of your `egui::Window`. + /// The difference with this and `.open(...)` is the ability to collapse the header, + /// it also will consume the event and won't borrow the passed parameter. + #[inline] + pub fn display_event(mut self, new_event: &mut Option) -> Self { + self.display_event = new_event.take(); + self + } + /// Set initial size of the window. #[inline] pub fn default_size(mut self, default_size: impl Into) -> Self { @@ -379,33 +422,66 @@ 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(WindowEvent::Hide) => collapsing.set_hidden(true), + Some(WindowEvent::Expand) => collapsing.set_open(true), + Some(WindowEvent::Collapse) => collapsing.set_open(false), + Some(WindowEvent::ToggleHidden) => collapsing.toggle_hidden(), + Some(WindowEvent::ToggleCollapse) => collapsing.set_open(!collapsing.is_open()), + None => match &open { + Some(false) => { + collapsing.set_hidden(true); + } + Some(true) => { + collapsing.set_hidden(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 @@ -450,6 +526,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); @@ -459,12 +536,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, ); @@ -500,7 +576,7 @@ impl<'open> Window<'open> { &mut area_content_ui, outer_rect, &content_response, - open, + with_closing_btn, &mut collapsing, collapsible, ); @@ -530,11 +606,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) + }) } } @@ -974,7 +1052,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, ) { @@ -983,11 +1061,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()); @@ -1017,7 +1093,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..595a77da667 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::{SetEvent, WindowEvent}; + #[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.set(WindowEvent::ToggleCollapse); + } + + pub fn toggle_hidden(&mut self) { + self.display_event.set(WindowEvent::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 14bf108f3ef..9cc44dc9764 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -271,6 +271,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 a90465cc19d..ac16d59cde4 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, "");