Collapsing header with custom header (#1538)

* Returns openness in CollapsingResponse
* Make CollapsingState a building block for custom collapsing headers
* Add a demo of the custom collapsing header
* Revert to much simpler tree demo
* Add CollapsingState::is_open and CollapsingState::set_open
This commit is contained in:
Emil Ernerfeldt 2022-04-28 11:09:44 +02:00 committed by GitHub
parent 8e266760e2
commit 39917bec26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 323 additions and 144 deletions

View file

@ -21,6 +21,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
* Added `Ui::spinner()` shortcut method ([#1494](https://github.com/emilk/egui/pull/1494)). * Added `Ui::spinner()` shortcut method ([#1494](https://github.com/emilk/egui/pull/1494)).
* Added `CursorIcon`s for resizing columns, rows, and the eight cardinal directions. * Added `CursorIcon`s for resizing columns, rows, and the eight cardinal directions.
* Added `Ui::toggle_value`. * Added `Ui::toggle_value`.
* Added ability to add any widgets to the header of a collapsing region ([#1538](https://github.com/emilk/egui/pull/1538)).
### Changed 🔧 ### Changed 🔧
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).

View file

@ -3,74 +3,194 @@ use std::hash::Hash;
use crate::*; use crate::*;
use epaint::Shape; use epaint::Shape;
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] pub(crate) struct InnerState {
pub(crate) struct State {
open: bool, open: bool,
/// Height of the region when open. Used for animations /// Height of the region when open. Used for animations
#[cfg_attr(feature = "serde", serde(default))]
open_height: Option<f32>, open_height: Option<f32>,
} }
impl State { /// This is a a building block for building collapsing regions.
///
/// It is used by [`CollapsingHeader`] and [`Window`], but can also be used on its own.
///
/// See [`CollapsingState::show_header`] for how to show a collapsing header with a custom header.
#[derive(Clone, Debug)]
pub struct CollapsingState {
id: Id,
state: InnerState,
}
impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> { pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id) ctx.data()
.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
} }
pub fn store(self, ctx: &Context, id: Id) { pub fn store(&self, ctx: &Context) {
ctx.data().insert_persisted(id, self); ctx.data().insert_persisted(self.id, self.state);
} }
pub fn from_memory_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self { pub fn id(&self) -> Id {
Self::load(ctx, id).unwrap_or_else(|| State { self.id
open: default_open, }
..Default::default()
pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
Self::load(ctx, id).unwrap_or(CollapsingState {
id,
state: InnerState {
open: default_open,
open_height: None,
},
}) })
} }
// Helper pub fn is_open(&self) -> bool {
pub fn is_open(ctx: &Context, id: Id) -> Option<bool> { self.state.open
if ctx.memory().everything_is_visible() { }
Some(true)
} else { pub fn set_open(&mut self, open: bool) {
State::load(ctx, id).map(|state| state.open) self.state.open = open;
}
} }
pub fn toggle(&mut self, ui: &Ui) { pub fn toggle(&mut self, ui: &Ui) {
self.open = !self.open; self.state.open = !self.state.open;
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
/// 0 for closed, 1 for open, with tweening /// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context, id: Id) -> f32 { pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory().everything_is_visible() { if ctx.memory().everything_is_visible() {
1.0 1.0
} else { } else {
ctx.animate_bool(id, self.open) ctx.animate_bool(self.id, self.state.open)
} }
} }
/// Show contents if we are open, with a nice animation between closed and open /// Will toggle when clicked, etc.
pub fn add_contents<R>( pub(crate) fn show_default_button_with_size(
&mut self, &mut self,
ui: &mut Ui, ui: &mut Ui,
id: Id, button_size: Vec2,
add_contents: impl FnOnce(&mut Ui) -> R, ) -> Response {
let (_id, rect) = ui.allocate_space(button_size);
let response = ui.interact(rect, self.id, Sense::click());
if response.clicked() {
self.toggle(ui);
}
let openness = self.openness(ui.ctx());
paint_default_icon(ui, openness, &response);
response
}
/// Will toggle when clicked, etc.
fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
let size = Vec2::new(ui.spacing().indent, ui.spacing().icon_width);
let (_id, rect) = ui.allocate_space(size);
let response = ui.interact(rect, self.id, Sense::click());
if response.clicked() {
self.toggle(ui);
}
let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect);
icon_rect.set_center(pos2(
response.rect.left() + ui.spacing().indent / 2.0,
response.rect.center().y,
));
let openness = self.openness(ui.ctx());
let small_icon_response = Response {
rect: icon_rect,
..response.clone()
};
paint_default_icon(ui, openness, &small_icon_response);
response
}
/// Shows header and body (if expanded).
///
/// The header will start with the default button in a horizontal layout, followed by whatever you add.
///
/// Will also store the state.
///
/// Returns the response of the collapsing button, the custom header, and the custom body.
///
/// ```
/// # 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)
/// .show_header(ui, |ui| {
/// ui.label("Header"); // you can put checkboxes or whatever here
/// })
/// .body(|ui| ui.label("Body"));
/// # });
/// ```
pub fn show_header<HeaderRet>(
mut self,
ui: &mut Ui,
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
) -> HeaderResponse<'_, HeaderRet> {
let header_response = ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
let collapser = self.show_default_button_indented(ui);
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
(collapser, add_header(ui))
});
HeaderResponse {
state: self,
ui,
toggle_button_response: header_response.inner.0,
header_response: InnerResponse {
response: header_response.response,
inner: header_response.inner.1,
},
}
}
/// Show body if we are open, with a nice animation between closed and open.
/// Indent the body to show it belongs to the header.
///
/// Will also store the state.
pub fn show_body_indented<R>(
&mut self,
header_response: &Response,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> { ) -> Option<InnerResponse<R>> {
let openness = self.openness(ui.ctx(), id); let id = self.id;
self.show_body_unindented(ui, |ui| {
ui.indent(id, |ui| {
// make as wide as the header:
ui.expand_to_include_x(header_response.rect.right());
add_body(ui)
})
.inner
})
}
/// Show body if we are open, with a nice animation between closed and open.
/// Will also store the state.
pub fn show_body_unindented<R>(
&mut self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let openness = self.openness(ui.ctx());
if openness <= 0.0 { if openness <= 0.0 {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None None
} else if openness < 1.0 { } else if openness < 1.0 {
Some(ui.scope(|child_ui| { Some(ui.scope(|child_ui| {
let max_height = if self.open && self.open_height.is_none() { let max_height = if self.state.open && self.state.open_height.is_none() {
// First frame of expansion. // First frame of expansion.
// We don't know full height yet, but we will next frame. // We don't know full height yet, but we will next frame.
// Just use a placeholder value that shows some movement: // Just use a placeholder value that shows some movement:
10.0 10.0
} else { } else {
let full_height = self.open_height.unwrap_or_default(); let full_height = self.state.open_height.unwrap_or_default();
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height) remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
}; };
@ -78,10 +198,11 @@ impl State {
clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height); clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
child_ui.set_clip_rect(clip_rect); child_ui.set_clip_rect(clip_rect);
let ret = add_contents(child_ui); let ret = add_body(child_ui);
let mut min_rect = child_ui.min_rect(); let mut min_rect = child_ui.min_rect();
self.open_height = Some(min_rect.height()); self.state.open_height = Some(min_rect.height());
self.store(child_ui.ctx()); // remember the height
// Pretend children took up at most `max_height` space: // Pretend children took up at most `max_height` space:
min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height); min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
@ -89,16 +210,49 @@ impl State {
ret ret
})) }))
} else { } else {
let ret_response = ui.scope(add_contents); let ret_response = ui.scope(add_body);
let full_size = ret_response.response.rect.size(); let full_size = ret_response.response.rect.size();
self.open_height = Some(full_size.y); self.state.open_height = Some(full_size.y);
self.store(ui.ctx()); // remember the height
Some(ret_response) Some(ret_response)
} }
} }
} }
/// From [`CollapsingState::show_header`].
#[must_use = "Remember to show the body"]
pub struct HeaderResponse<'ui, HeaderRet> {
state: CollapsingState,
ui: &'ui mut Ui,
toggle_button_response: Response,
header_response: InnerResponse<HeaderRet>,
}
impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
/// Returns the response of the collapsing button, the custom header, and the custom body.
pub fn body<BodyRet>(
mut self,
add_body: impl FnOnce(&mut Ui) -> BodyRet,
) -> (
Response,
InnerResponse<HeaderRet>,
Option<InnerResponse<BodyRet>>,
) {
let body_response =
self.state
.show_body_indented(&self.header_response.response, self.ui, add_body);
(
self.toggle_button_response,
self.header_response,
body_response,
)
}
}
// ----------------------------------------------------------------------------
/// Paint the arrow icon that indicated if the region is open or not /// Paint the arrow icon that indicated if the region is open or not
pub(crate) fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) { pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
let visuals = ui.style().interact(response); let visuals = ui.style().interact(response);
let stroke = visuals.fg_stroke; let stroke = visuals.fg_stroke;
@ -126,13 +280,15 @@ pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// egui::CollapsingHeader::new("Heading") /// egui::CollapsingHeader::new("Heading")
/// .show(ui, |ui| { /// .show(ui, |ui| {
/// ui.label("Contents"); /// ui.label("Body");
/// }); /// });
/// ///
/// // Short version: /// // Short version:
/// ui.collapsing("Heading", |ui| { ui.label("Contents"); }); /// ui.collapsing("Heading", |ui| { ui.label("Body"); });
/// # }); /// # });
/// ``` /// ```
///
/// If you want to customize the header contents, see [`CollapsingState::show_header`].
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct CollapsingHeader { pub struct CollapsingHeader {
text: WidgetText, text: WidgetText,
@ -217,7 +373,7 @@ impl CollapsingHeader {
/// let response = egui::CollapsingHeader::new("Select and open me") /// let response = egui::CollapsingHeader::new("Select and open me")
/// .selectable(true) /// .selectable(true)
/// .selected(selected) /// .selected(selected)
/// .show(ui, |ui| ui.label("Content")); /// .show(ui, |ui| ui.label("Body"));
/// if response.header_response.clicked() { /// if response.header_response.clicked() {
/// selected = true; /// selected = true;
/// } /// }
@ -265,9 +421,9 @@ impl CollapsingHeader {
} }
struct Prepared { struct Prepared {
id: Id,
header_response: Response, header_response: Response,
state: State, state: CollapsingState,
openness: f32,
} }
impl CollapsingHeader { impl CollapsingHeader {
@ -283,9 +439,9 @@ impl CollapsingHeader {
open, open,
id_source, id_source,
enabled: _, enabled: _,
selectable: _, selectable,
selected: _, selected,
show_background: _, show_background,
} = self; } = self;
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame. // TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
@ -315,9 +471,9 @@ impl CollapsingHeader {
header_response.rect.center().y - text.size().y / 2.0, header_response.rect.center().y - text.size().y / 2.0,
); );
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
if let Some(open) = open { if let Some(open) = open {
if open != state.open { if open != state.is_open() {
state.toggle(ui); state.toggle(ui);
header_response.mark_changed(); header_response.mark_changed();
} }
@ -329,12 +485,12 @@ impl CollapsingHeader {
header_response header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text())); .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text()));
if ui.is_rect_visible(rect) { let openness = state.openness(ui.ctx());
let visuals = ui
.style()
.interact_selectable(&header_response, self.selected);
if ui.visuals().collapsing_header_frame || self.show_background { if ui.is_rect_visible(rect) {
let visuals = ui.style().interact_selectable(&header_response, selected);
if ui.visuals().collapsing_header_frame || show_background {
ui.painter().add(epaint::RectShape { ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion), rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding, rounding: visuals.rounding,
@ -344,8 +500,7 @@ impl CollapsingHeader {
}); });
} }
if self.selected if selected || selectable && (header_response.hovered() || header_response.has_focus())
|| self.selectable && (header_response.hovered() || header_response.has_focus())
{ {
let rect = rect.expand(visuals.expansion); let rect = rect.expand(visuals.expansion);
@ -363,7 +518,6 @@ impl CollapsingHeader {
rect: icon_rect, rect: icon_rect,
..header_response.clone() ..header_response.clone()
}; };
let openness = state.openness(ui.ctx(), id);
if let Some(icon) = icon { if let Some(icon) = icon {
icon(ui, openness, &icon_response); icon(ui, openness, &icon_response);
} else { } else {
@ -375,9 +529,9 @@ impl CollapsingHeader {
} }
Prepared { Prepared {
id,
header_response, header_response,
state, state,
openness,
} }
} }
@ -385,48 +539,42 @@ impl CollapsingHeader {
pub fn show<R>( pub fn show<R>(
self, self,
ui: &mut Ui, ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R, add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_contents)) self.show_dyn(ui, Box::new(add_body))
} }
fn show_dyn<'c, R>( fn show_dyn<'c, R>(
self, self,
ui: &mut Ui, ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
// Make sure contents are bellow header, // Make sure body is bellow header,
// and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid). // and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid).
ui.vertical(|ui| { ui.vertical(|ui| {
ui.set_enabled(self.enabled); ui.set_enabled(self.enabled);
let Prepared { let Prepared {
id,
header_response, header_response,
mut state, mut state,
} = self.begin(ui); openness,
} = self.begin(ui); // show the header
let ret_response = state.add_contents(ui, id, |ui| { let ret_response = state.show_body_indented(&header_response, ui, add_body);
ui.indent(id, |ui| {
// make as wide as the header:
ui.expand_to_include_x(header_response.rect.right());
add_contents(ui)
})
.inner
});
state.store(ui.ctx(), id);
if let Some(ret_response) = ret_response { if let Some(ret_response) = ret_response {
CollapsingResponse { CollapsingResponse {
header_response, header_response,
body_response: Some(ret_response.response), body_response: Some(ret_response.response),
body_returned: Some(ret_response.inner), body_returned: Some(ret_response.inner),
openness,
} }
} else { } else {
CollapsingResponse { CollapsingResponse {
header_response, header_response,
body_response: None, body_response: None,
body_returned: None, body_returned: None,
openness,
} }
} }
}) })
@ -436,9 +584,27 @@ impl CollapsingHeader {
/// The response from showing a [`CollapsingHeader`]. /// The response from showing a [`CollapsingHeader`].
pub struct CollapsingResponse<R> { pub struct CollapsingResponse<R> {
/// Response of the actual clickable header.
pub header_response: Response, pub header_response: Response,
/// None iff collapsed. /// None iff collapsed.
pub body_response: Option<Response>, pub body_response: Option<Response>,
/// None iff collapsed. /// None iff collapsed.
pub body_returned: Option<R>, pub body_returned: Option<R>,
/// 0.0 if fully closed, 1.0 if fully open, and something in-between while animating.
pub openness: f32,
}
impl<R> CollapsingResponse<R> {
/// Was the [`CollapsingHeader`] fully closed (and not being animated)?
pub fn fully_closed(&self) -> bool {
self.openness <= 0.0
}
/// Was the [`CollapsingHeader`] fully open (and not being animated)?
pub fn fully_open(&self) -> bool {
self.openness >= 1.0
}
} }

View file

@ -3,7 +3,7 @@
//! For instance, a [`Frame`] adds a frame and background to some contained UI. //! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area; pub(crate) mod area;
pub(crate) mod collapsing_header; pub mod collapsing_header;
mod combo_box; mod combo_box;
pub(crate) mod frame; pub(crate) mod frame;
pub mod panel; pub mod panel;

View file

@ -1,5 +1,6 @@
// WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts. // WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts.
use crate::collapsing_header::CollapsingState;
use crate::{widget_text::WidgetTextGalley, *}; use crate::{widget_text::WidgetTextGalley, *};
use epaint::*; use epaint::*;
@ -269,10 +270,10 @@ impl<'open> Window<'open> {
let area_id = area.id; let area_id = area.id;
let area_layer_id = area.layer(); let area_layer_id = area.layer();
let resize_id = area_id.with("resize"); let resize_id = area_id.with("resize");
let collapsing_id = area_id.with("collapsing"); let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
let is_collapsed = with_title_bar let is_collapsed = with_title_bar && !collapsing.is_open();
&& !collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed); 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 let area = area.movable(false); // We move it manually, or the area will move the window when we want to resize it
@ -326,19 +327,12 @@ impl<'open> Window<'open> {
let frame_stroke = frame.stroke; let frame_stroke = frame.stroke;
let mut frame = frame.begin(&mut area_content_ui); let mut frame = frame.begin(&mut area_content_ui);
let default_expanded = true;
let mut collapsing = collapsing_header::State::from_memory_with_default_open(
ctx,
collapsing_id,
default_expanded,
);
let show_close_button = open.is_some(); let show_close_button = open.is_some();
let title_bar = if with_title_bar { let title_bar = if with_title_bar {
let title_bar = show_title_bar( let title_bar = show_title_bar(
&mut frame.content_ui, &mut frame.content_ui,
title, title,
show_close_button, show_close_button,
collapsing_id,
&mut collapsing, &mut collapsing,
collapsible, collapsible,
); );
@ -349,7 +343,7 @@ impl<'open> Window<'open> {
}; };
let (content_inner, content_response) = collapsing let (content_inner, content_response) = collapsing
.add_contents(&mut frame.content_ui, collapsing_id, |ui| { .show_body_unindented(&mut frame.content_ui, |ui| {
resize.show(ui, |ui| { resize.show(ui, |ui| {
if title_bar.is_some() { if title_bar.is_some() {
ui.add_space(title_content_spacing); ui.add_space(title_content_spacing);
@ -380,7 +374,7 @@ impl<'open> Window<'open> {
); );
} }
collapsing.store(ctx, collapsing_id); collapsing.store(ctx);
if let Some(interaction) = interaction { if let Some(interaction) = interaction {
paint_frame_interaction( paint_frame_interaction(
@ -781,8 +775,7 @@ fn show_title_bar(
ui: &mut Ui, ui: &mut Ui,
title: WidgetText, title: WidgetText,
show_close_button: bool, show_close_button: bool,
collapsing_id: Id, collapsing: &mut CollapsingState,
collapsing: &mut collapsing_header::State,
collapsible: bool, collapsible: bool,
) -> TitleBar { ) -> TitleBar {
let inner_response = ui.horizontal(|ui| { let inner_response = ui.horizontal(|ui| {
@ -798,14 +791,7 @@ fn show_title_bar(
if collapsible { if collapsible {
ui.add_space(pad); ui.add_space(pad);
collapsing.show_default_button_with_size(ui, button_size);
let (_id, rect) = ui.allocate_space(button_size);
let collapse_button_response = ui.interact(rect, collapsing_id, Sense::click());
if collapse_button_response.clicked() {
collapsing.toggle(ui);
}
let openness = collapsing.openness(ui.ctx(), collapsing_id);
collapsing_header::paint_default_icon(ui, openness, &collapse_button_response);
} }
let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading); let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading);
@ -854,7 +840,7 @@ impl TitleBar {
outer_rect: Rect, outer_rect: Rect,
content_response: &Option<Response>, content_response: &Option<Response>,
open: Option<&mut bool>, open: Option<&mut bool>,
collapsing: &mut collapsing_header::State, collapsing: &mut CollapsingState,
collapsible: bool, collapsible: bool,
) { ) {
if let Some(content_response) = &content_response { if let Some(content_response) = &content_response {

View file

@ -1238,11 +1238,12 @@ impl Context {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!( ui.label(format!(
"{} collapsing headers", "{} collapsing headers",
self.data().count::<containers::collapsing_header::State>() self.data()
.count::<containers::collapsing_header::InnerState>()
)); ));
if ui.button("Reset").clicked() { if ui.button("Reset").clicked() {
self.data() self.data()
.remove_by_type::<containers::collapsing_header::State>(); .remove_by_type::<containers::collapsing_header::InnerState>();
} }
}); });

View file

@ -267,9 +267,13 @@ pub struct Spacing {
pub text_edit_width: f32, pub text_edit_width: f32,
/// Checkboxes, radio button and collapsing headers have an icon at the start. /// Checkboxes, radio button and collapsing headers have an icon at the start.
/// This is the width/height of this icon. /// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
pub icon_width: f32, pub icon_width: f32,
/// Checkboxes, radio button and collapsing headers have an icon at the start.
/// This is the width/height of the inner part of this icon (e.g. the check of the checkbox).
pub icon_width_inner: f32,
/// Checkboxes, radio button and collapsing headers have an icon at the start. /// Checkboxes, radio button and collapsing headers have an icon at the start.
/// This is the spacing between the icon and the text /// This is the spacing between the icon and the text
pub icon_spacing: f32, pub icon_spacing: f32,
@ -289,15 +293,14 @@ pub struct Spacing {
impl Spacing { impl Spacing {
/// Returns small icon rectangle and big icon rectangle /// Returns small icon rectangle and big icon rectangle
pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) { pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) {
let box_side = self.icon_width; let icon_width = self.icon_width;
let big_icon_rect = Rect::from_center_size( let big_icon_rect = Rect::from_center_size(
pos2(rect.left() + box_side / 2.0, rect.center().y), pos2(rect.left() + icon_width / 2.0, rect.center().y),
vec2(box_side, box_side), vec2(icon_width, icon_width),
); );
let small_rect_side = 8.0; // TODO: make a parameter
let small_icon_rect = let small_icon_rect =
Rect::from_center_size(big_icon_rect.center(), Vec2::splat(small_rect_side)); Rect::from_center_size(big_icon_rect.center(), Vec2::splat(self.icon_width_inner));
(small_icon_rect, big_icon_rect) (small_icon_rect, big_icon_rect)
} }
@ -634,6 +637,7 @@ impl Default for Spacing {
slider_width: 100.0, slider_width: 100.0,
text_edit_width: 280.0, text_edit_width: 280.0,
icon_width: 14.0, icon_width: 14.0,
icon_width_inner: 8.0,
icon_spacing: 4.0, icon_spacing: 4.0,
tooltip_width: 600.0, tooltip_width: 600.0,
combo_height: 200.0, combo_height: 200.0,
@ -909,6 +913,7 @@ impl Spacing {
slider_width, slider_width,
text_edit_width, text_edit_width,
icon_width, icon_width,
icon_width_inner,
icon_spacing, icon_spacing,
tooltip_width, tooltip_width,
indent_ends_with_horizontal_line, indent_ends_with_horizontal_line,
@ -972,7 +977,12 @@ impl Spacing {
ui.label("Checkboxes etc:"); ui.label("Checkboxes etc:");
ui.add( ui.add(
DragValue::new(icon_width) DragValue::new(icon_width)
.prefix("width:") .prefix("outer icon width:")
.clamp_range(0.0..=60.0),
);
ui.add(
DragValue::new(icon_width_inner)
.prefix("inner icon width:")
.clamp_range(0.0..=60.0), .clamp_range(0.0..=60.0),
); );
ui.add( ui.add(

View file

@ -14,6 +14,7 @@ pub struct MiscDemoWindow {
widgets: Widgets, widgets: Widgets,
colors: ColorWidgets, colors: ColorWidgets,
custom_collapsing_header: CustomCollapsingHeader,
tree: Tree, tree: Tree,
box_painting: BoxPainting, box_painting: BoxPainting,
@ -32,6 +33,7 @@ impl Default for MiscDemoWindow {
widgets: Default::default(), widgets: Default::default(),
colors: Default::default(), colors: Default::default(),
custom_collapsing_header: Default::default(),
tree: Tree::demo(), tree: Tree::demo(),
box_painting: Default::default(), box_painting: Default::default(),
@ -82,6 +84,10 @@ impl View for MiscDemoWindow {
self.colors.ui(ui); self.colors.ui(ui);
}); });
CollapsingHeader::new("Custom Collapsing Header")
.default_open(false)
.show(ui, |ui| self.custom_collapsing_header.ui(ui));
CollapsingHeader::new("Tree") CollapsingHeader::new("Tree")
.default_open(false) .default_open(false)
.show(ui, |ui| self.tree.ui(ui)); .show(ui, |ui| self.tree.ui(ui));
@ -351,6 +357,44 @@ impl BoxPainting {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct CustomCollapsingHeader {
selected: bool,
radio_value: bool,
}
impl Default for CustomCollapsingHeader {
fn default() -> Self {
Self {
selected: true,
radio_value: false,
}
}
}
impl CustomCollapsingHeader {
pub fn ui(&mut self, ui: &mut egui::Ui) {
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)
.show_header(ui, |ui| {
ui.toggle_value(&mut self.selected, "Click to select/unselect");
ui.radio_value(&mut self.radio_value, false, "");
ui.radio_value(&mut self.radio_value, true, "");
})
.body(|ui| {
ui.label("The body is always custom");
});
CollapsingHeader::new("Normal collapsing header for comparison").show(ui, |ui| {
ui.label("Nothing exciting here");
});
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
enum Action { enum Action {
Keep, Keep,
@ -359,53 +403,30 @@ enum Action {
#[derive(Clone, Default)] #[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct Tree(String, SubTree); struct Tree(Vec<Tree>);
impl Tree { impl Tree {
pub fn demo() -> Self { pub fn demo() -> Self {
Self( Self(vec![
String::from("root"), Tree(vec![Tree::default(); 4]),
SubTree(vec![ Tree(vec![Tree(vec![Tree::default(); 2]); 3]),
SubTree(vec![SubTree::default(); 4]), ])
SubTree(vec![SubTree(vec![SubTree::default(); 2]); 3]),
]),
)
} }
pub fn ui(&mut self, ui: &mut Ui) -> Action { pub fn ui(&mut self, ui: &mut Ui) -> Action {
self.1.ui(ui, 0, "root", &mut self.0) self.ui_impl(ui, 0, "root")
} }
} }
#[derive(Clone, Default)] impl Tree {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] fn ui_impl(&mut self, ui: &mut Ui, depth: usize, name: &str) -> Action {
struct SubTree(Vec<SubTree>); CollapsingHeader::new(name)
impl SubTree {
pub fn ui(
&mut self,
ui: &mut Ui,
depth: usize,
name: &str,
selected_name: &mut String,
) -> Action {
let response = CollapsingHeader::new(name)
.default_open(depth < 1) .default_open(depth < 1)
.selectable(true) .show(ui, |ui| self.children_ui(ui, depth))
.selected(selected_name.as_str() == name) .body_returned
.show(ui, |ui| self.children_ui(ui, name, depth, selected_name)); .unwrap_or(Action::Keep)
if response.header_response.clicked() {
*selected_name = name.to_string();
}
response.body_returned.unwrap_or(Action::Keep)
} }
fn children_ui( fn children_ui(&mut self, ui: &mut Ui, depth: usize) -> Action {
&mut self,
ui: &mut Ui,
parent_name: &str,
depth: usize,
selected_name: &mut String,
) -> Action {
if depth > 0 if depth > 0
&& ui && ui
.button(RichText::new("delete").color(Color32::RED)) .button(RichText::new("delete").color(Color32::RED))
@ -419,13 +440,7 @@ impl SubTree {
.into_iter() .into_iter()
.enumerate() .enumerate()
.filter_map(|(i, mut tree)| { .filter_map(|(i, mut tree)| {
if tree.ui( if tree.ui_impl(ui, depth + 1, &format!("child #{}", i)) == Action::Keep {
ui,
depth + 1,
&format!("{}/{}", parent_name, i),
selected_name,
) == Action::Keep
{
Some(tree) Some(tree)
} else { } else {
None None
@ -434,7 +449,7 @@ impl SubTree {
.collect(); .collect();
if ui.button("+").clicked() { if ui.button("+").clicked() {
self.0.push(SubTree::default()); self.0.push(Tree::default());
} }
Action::Keep Action::Keep