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 `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)).
|
||||
|
|
|
@ -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()));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let visuals = ui
|
||||
.style()
|
||||
.interact_selectable(&header_response, self.selected);
|
||||
let openness = state.openness(ui.ctx());
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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…
Reference in a new issue