Skip to content

Commit

Permalink
Add programmable show/hide, expand/collapse actions on windows.
Browse files Browse the repository at this point in the history
Because this is programmable now, pass the closing button as a value.

Change some of the 'open' naming to 'show/hide' because the open keyword
was used both for expanded and for shown. This makes a better
distinction
  • Loading branch information
Norlock committed Jul 16, 2024
1 parent 527b247 commit 5116a19
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 48 deletions.
66 changes: 54 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,57 @@ 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.shown = true;
self.state.expanded = true;
}
WindowAction::Collapse => {
self.state.shown = true;
self.state.expanded = true;
}
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 +109,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 +233,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 +309,17 @@ pub struct HeaderResponse<'ui, HeaderRet> {
}

impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
#[deprecated = "Use het `HeaderResponse::is_expanded`"]
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 +572,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},
};
116 changes: 82 additions & 34 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::sync::Arc;
use crate::collapsing_header::CollapsingState;
use crate::*;
use epaint::*;
use serde::{Deserialize, Serialize};

use super::*;

Expand Down Expand Up @@ -32,7 +33,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 +46,18 @@ pub struct Window<'open> {
fade_out: bool,
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
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 +67,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 +80,7 @@ impl<'open> Window<'open> {
default_open: true,
with_title_bar: true,
fade_out: true,
show_close_button: false,
}
}

Expand All @@ -74,11 +91,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, show, hide the window.
/// The 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 +455,7 @@ impl<'open> Window<'open> {
) -> Option<InnerResponse<Option<R>>> {
let Window {
title,
window_action,
open,
area,
frame,
Expand All @@ -429,6 +465,7 @@ impl<'open> Window<'open> {
default_open,
with_title_bar,
fade_out,
show_close_button,
} = self;

let header_color =
Expand All @@ -440,25 +477,37 @@ 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 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);
//println!("{collapsing:?}");

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 +555,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 +567,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 +607,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 +635,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 +655,9 @@ impl<'open> Window<'open> {
&mut area_content_ui,
title_rect,
&content_response,
open,
&mut collapsing,
collapsible,
show_close_button,
);
}

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

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

let inner_response = InnerResponse {
// Resync if open & window_action are just 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 +743,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 +1163,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

0 comments on commit 5116a19

Please sign in to comment.