Browse Source

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
pull/1540/head
Emil Ernerfeldt 3 years ago
committed by GitHub
parent
commit
39917bec26
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 294
      egui/src/containers/collapsing_header.rs
  3. 2
      egui/src/containers/mod.rs
  4. 32
      egui/src/containers/window.rs
  5. 5
      egui/src/context.rs
  6. 24
      egui/src/style.rs
  7. 103
      egui_demo_lib/src/apps/demo/misc_demo_window.rs

1
CHANGELOG.md

@ -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 `CursorIcon`s for resizing columns, rows, and the eight cardinal directions.
* 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 🔧
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).

294
egui/src/containers/collapsing_header.rs

@ -3,74 +3,194 @@ use std::hash::Hash;
use crate::*;
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", serde(default))]
pub(crate) struct State {
pub(crate) struct InnerState {
open: bool,
/// Height of the region when open. Used for animations
#[cfg_attr(feature = "serde", serde(default))]
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> {
ctx.data().get_persisted(id)
ctx.data()
.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
pub fn store(&self, ctx: &Context) {
ctx.data().insert_persisted(self.id, self.state);
}
pub fn from_memory_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
Self::load(ctx, id).unwrap_or_else(|| State {
pub fn id(&self) -> Id {
self.id
}
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,
..Default::default()
open_height: None,
},
})
}
// Helper
pub fn is_open(ctx: &Context, id: Id) -> Option<bool> {
if ctx.memory().everything_is_visible() {
Some(true)
} else {
State::load(ctx, id).map(|state| state.open)
pub fn is_open(&self) -> bool {
self.state.open
}
pub fn set_open(&mut self, open: bool) {
self.state.open = open;
}
pub fn toggle(&mut self, ui: &Ui) {
self.open = !self.open;
self.state.open = !self.state.open;
ui.ctx().request_repaint();
}
/// 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() {
1.0
} 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
pub fn add_contents<R>(
/// Will toggle when clicked, etc.
pub(crate) fn show_default_button_with_size(
&mut self,
ui: &mut Ui,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
button_size: Vec2,
) -> 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>> {
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 {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None
} else if openness < 1.0 {
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.
// We don't know full height yet, but we will next frame.
// Just use a placeholder value that shows some movement:
10.0
} 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)
};
@ -78,10 +198,11 @@ impl State {
clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
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();
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:
min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
@ -89,16 +210,49 @@ impl State {
ret
}))
} else {
let ret_response = ui.scope(add_contents);
let ret_response = ui.scope(add_body);
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)
}
}
}
/// 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
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 stroke = visuals.fg_stroke;
@ -126,13 +280,15 @@ pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
/// # egui::__run_test_ui(|ui| {
/// egui::CollapsingHeader::new("Heading")
/// .show(ui, |ui| {
/// ui.label("Contents");
/// ui.label("Body");
/// });
///
/// // 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()"]
pub struct CollapsingHeader {
text: WidgetText,
@ -217,7 +373,7 @@ impl CollapsingHeader {
/// let response = egui::CollapsingHeader::new("Select and open me")
/// .selectable(true)
/// .selected(selected)
/// .show(ui, |ui| ui.label("Content"));
/// .show(ui, |ui| ui.label("Body"));
/// if response.header_response.clicked() {
/// selected = true;
/// }
@ -265,9 +421,9 @@ impl CollapsingHeader {
}
struct Prepared {
id: Id,
header_response: Response,
state: State,
state: CollapsingState,
openness: f32,
}
impl CollapsingHeader {
@ -283,9 +439,9 @@ impl CollapsingHeader {
open,
id_source,
enabled: _,
selectable: _,
selected: _,
show_background: _,
selectable,
selected,
show_background,
} = self;
// 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,
);
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 open != state.open {
if open != state.is_open() {
state.toggle(ui);
header_response.mark_changed();
}
@ -329,12 +485,12 @@ impl CollapsingHeader {
header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text()));
let openness = state.openness(ui.ctx());
if ui.is_rect_visible(rect) {
let visuals = ui
.style()
.interact_selectable(&header_response, self.selected);
let visuals = ui.style().interact_selectable(&header_response, selected);
if ui.visuals().collapsing_header_frame || self.show_background {
if ui.visuals().collapsing_header_frame || show_background {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
@ -344,8 +500,7 @@ impl CollapsingHeader {
});
}
if self.selected
|| self.selectable && (header_response.hovered() || header_response.has_focus())
if selected || selectable && (header_response.hovered() || header_response.has_focus())
{
let rect = rect.expand(visuals.expansion);
@ -363,7 +518,6 @@ impl CollapsingHeader {
rect: icon_rect,
..header_response.clone()
};
let openness = state.openness(ui.ctx(), id);
if let Some(icon) = icon {
icon(ui, openness, &icon_response);
} else {
@ -375,9 +529,9 @@ impl CollapsingHeader {
}
Prepared {
id,
header_response,
state,
openness,
}
}
@ -385,48 +539,42 @@ impl CollapsingHeader {
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_contents))
self.show_dyn(ui, Box::new(add_body))
}
fn show_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> 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).
ui.vertical(|ui| {
ui.set_enabled(self.enabled);
let Prepared {
id,
header_response,
mut state,
} = self.begin(ui);
openness,
} = self.begin(ui); // show the header
let ret_response = state.add_contents(ui, id, |ui| {
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);
let ret_response = state.show_body_indented(&header_response, ui, add_body);
if let Some(ret_response) = ret_response {
CollapsingResponse {
header_response,
body_response: Some(ret_response.response),
body_returned: Some(ret_response.inner),
openness,
}
} else {
CollapsingResponse {
header_response,
body_response: None,
body_returned: None,
openness,
}
}
})
@ -436,9 +584,27 @@ impl CollapsingHeader {
/// The response from showing a [`CollapsingHeader`].
pub struct CollapsingResponse<R> {
/// Response of the actual clickable header.
pub header_response: Response,
/// None iff collapsed.
pub body_response: Option<Response>,
/// None iff collapsed.
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
}
}

2
egui/src/containers/mod.rs

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

32
egui/src/containers/window.rs

@ -1,5 +1,6 @@
// 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 epaint::*;
@ -269,10 +270,10 @@ impl<'open> Window<'open> {
let area_id = area.id;
let area_layer_id = area.layer();
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
&& !collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default();
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
@ -326,19 +327,12 @@ impl<'open> Window<'open> {
let frame_stroke = frame.stroke;
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 title_bar = if with_title_bar {
let title_bar = show_title_bar(
&mut frame.content_ui,
title,
show_close_button,
collapsing_id,
&mut collapsing,
collapsible,
);
@ -349,7 +343,7 @@ impl<'open> Window<'open> {
};
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| {
if title_bar.is_some() {
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 {
paint_frame_interaction(
@ -781,8 +775,7 @@ fn show_title_bar(
ui: &mut Ui,
title: WidgetText,
show_close_button: bool,
collapsing_id: Id,
collapsing: &mut collapsing_header::State,
collapsing: &mut CollapsingState,
collapsible: bool,
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
@ -798,14 +791,7 @@ fn show_title_bar(
if collapsible {
ui.add_space(pad);
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);
collapsing.show_default_button_with_size(ui, button_size);
}
let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading);
@ -854,7 +840,7 @@ impl TitleBar {
outer_rect: Rect,
content_response: &Option<Response>,
open: Option<&mut bool>,
collapsing: &mut collapsing_header::State,
collapsing: &mut CollapsingState,
collapsible: bool,
) {
if let Some(content_response) = &content_response {

5
egui/src/context.rs

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

24
egui/src/style.rs

@ -267,9 +267,13 @@ pub struct Spacing {
pub text_edit_width: f32,
/// 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,
/// 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.
/// This is the spacing between the icon and the text
pub icon_spacing: f32,
@ -289,15 +293,14 @@ pub struct Spacing {
impl Spacing {
/// Returns small icon rectangle and big icon rectangle
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(
pos2(rect.left() + box_side / 2.0, rect.center().y),
vec2(box_side, box_side),
pos2(rect.left() + icon_width / 2.0, rect.center().y),
vec2(icon_width, icon_width),
);
let small_rect_side = 8.0; // TODO: make a parameter
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)
}
@ -634,6 +637,7 @@ impl Default for Spacing {
slider_width: 100.0,
text_edit_width: 280.0,
icon_width: 14.0,
icon_width_inner: 8.0,
icon_spacing: 4.0,
tooltip_width: 600.0,
combo_height: 200.0,
@ -909,6 +913,7 @@ impl Spacing {
slider_width,
text_edit_width,
icon_width,
icon_width_inner,
icon_spacing,
tooltip_width,
indent_ends_with_horizontal_line,
@ -972,7 +977,12 @@ impl Spacing {
ui.label("Checkboxes etc:");
ui.add(
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),
);
ui.add(

103
egui_demo_lib/src/apps/demo/misc_demo_window.rs

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

Loading…
Cancel
Save