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:
parent
8e266760e2
commit
39917bec26
7 changed files with 323 additions and 144 deletions
|
@ -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)).
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue