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

Programmatically open / closing windows #3517

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
44 changes: 28 additions & 16 deletions crates/egui/src/containers/collapsing_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand All @@ -25,13 +26,6 @@ pub struct CollapsingState {
}

impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| {
d.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
})
}

pub fn store(&self, ctx: &Context) {
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
}
Expand All @@ -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::<InnerState>(id)
.map(|state| Self { id, state })
})
.unwrap_or(CollapsingState {
id,
state: InnerState {
open: default_open,
hidden: false,
open_height: None,
},
})
Expand All @@ -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;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean for a CollapsingHeader to be hidden?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well windows can become hidden, collapsing headers won't do anything but I will update the PR, so this code won't be accessible when not using a window. I also have made the same event option for normal widgets to expand/collapse which I will include in this PR as well.

Copy link
Author

@Norlock Norlock Nov 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some comments to notify the dev that its only for window components. The API is not open to end users, I have made another branch where I split the two structs (see comments below).

Copy link
Author

@Norlock Norlock Nov 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I build a split between the two states on this branch (draft): Norlock@40b1fb7#diff-817170c50650af7962ada22bb6459b20eeb54ce921043e4db8522368ccf8933e. I don't know if its a real improvement but you can check it out. If you prefer that version I can clean up the code a bit


/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory(|mem| mem.everything_is_visible()) {
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}

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::{DisplayEvent, SetEvent, Window},
};
110 changes: 87 additions & 23 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,38 @@ use super::*;
#[must_use = "You should call .show()"]
pub struct Window<'open> {
title: WidgetText,
open: Option<&'open mut bool>,
area: Area,
frame: Option<Frame>,
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
default_open: bool,
with_title_bar: bool,
with_closing_btn: bool,
display_event: Option<DisplayEvent>,
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,
Comment on lines +49 to +53
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need docstrings.

What does it mean to hide a window? Does it mean "Close"? If so, please name it that because "Close" is the antonym of "Open".

}

// Helper function for user space
pub trait SetEvent {
Comment on lines +56 to +57
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetWindowEvent is probably a better name.

What would this be used for?

fn set(&mut self, display_event: DisplayEvent);
}

impl SetEvent for Option<DisplayEvent> {
fn set(&mut self, display_event: DisplayEvent) {
let _ = self.insert(display_event);
}
}

impl<'open> Window<'open> {
Expand All @@ -44,6 +68,7 @@ impl<'open> Window<'open> {
pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text())).constrain(true);

Self {
title,
open: None,
Expand All @@ -55,8 +80,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,
}
}

Expand All @@ -71,8 +98,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
}

Expand Down Expand Up @@ -205,6 +239,11 @@ impl<'open> Window<'open> {
self
}

pub fn display_event(mut self, new_event: &mut Option<DisplayEvent>) -> Self {
self.display_event = new_event.take();
self
}

/// Set initial size of the window.
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.default_size(default_size);
Expand Down Expand Up @@ -317,33 +356,58 @@ impl<'open> Window<'open> {
) -> Option<InnerResponse<Option<R>>> {
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
Expand Down Expand Up @@ -388,6 +452,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);
Expand All @@ -397,12 +462,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,
);
Expand Down Expand Up @@ -438,7 +502,7 @@ impl<'open> Window<'open> {
&mut area_content_ui,
outer_rect,
&content_response,
open,
with_closing_btn,
&mut collapsing,
collapsible,
);
Expand Down Expand Up @@ -468,11 +532,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)
})
}
}

Expand Down Expand Up @@ -907,7 +973,7 @@ impl TitleBar {
ui: &mut Ui,
outer_rect: Rect,
content_response: &Option<Response>,
open: Option<&mut bool>,
with_closing_btn: bool,
collapsing: &mut CollapsingState,
collapsible: bool,
) {
Expand All @@ -916,11 +982,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());
Expand Down Expand Up @@ -950,7 +1014,7 @@ impl TitleBar {
.double_clicked()
&& collapsible
{
collapsing.toggle(ui);
collapsing.toggle_open(ui);
}
}

Expand Down
17 changes: 16 additions & 1 deletion crates/egui_demo_lib/src/demo/about.rs
Original file line number Diff line number Diff line change
@@ -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<DisplayEvent>,
}

impl super::Demo for About {
fn name(&self) -> &'static str {
Expand All @@ -13,13 +17,24 @@ 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);
});
}
}

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};
Expand Down
6 changes: 6 additions & 0 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});

Expand Down
Loading