egui/emigui/src/containers/collapsing_header.rs

191 lines
6.2 KiB
Rust
Raw Normal View History

use crate::{layout::Direction, widgets::Label, *};
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
#[serde(default)]
pub(crate) struct State {
open: bool,
2020-05-05 01:05:36 +00:00
#[serde(skip)] // Times are relative, and we don't want to continue animations anyway
toggle_time: f64,
2020-05-05 17:41:49 +00:00
2020-05-08 20:42:31 +00:00
/// Height of the region when open. Used for animations
2020-05-05 17:41:49 +00:00
open_height: Option<f32>,
}
impl Default for State {
fn default() -> Self {
Self {
open: false,
toggle_time: -f64::INFINITY,
2020-05-05 17:41:49 +00:00
open_height: None,
}
}
}
pub struct CollapsingHeader {
label: Label,
default_open: bool,
}
impl CollapsingHeader {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: Label::new(label)
.text_style(TextStyle::Button)
.multiline(false),
default_open: false,
}
}
pub fn default_open(mut self) -> Self {
self.default_open = true;
self
}
}
impl CollapsingHeader {
2020-05-08 20:42:31 +00:00
pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) -> GuiResponse {
assert!(
2020-05-08 20:42:31 +00:00
ui.direction() == Direction::Vertical,
"Horizontal collapsing is unimplemented"
);
let Self {
label,
default_open,
} = self;
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
let title = &label.text; // TODO: not this
2020-05-08 20:42:31 +00:00
let id = ui.make_unique_id(title);
let text_pos = ui.cursor() + vec2(ui.style().indent, 0.0);
let (title, text_size) = label.layout(text_pos, ui);
let text_max_x = text_pos.x + text_size.x;
let desired_width = ui
.available_finite()
.size()
.x
.max(text_max_x - ui.cursor().x);
2020-05-08 20:42:31 +00:00
let interact = ui.reserve_space(
vec2(
desired_width,
2020-05-08 20:42:31 +00:00
text_size.y + 2.0 * ui.style().button_padding.y,
),
Some(id),
);
let text_pos = pos2(text_pos.x, interact.rect.center().y - text_size.y / 2.0);
2020-05-05 17:41:49 +00:00
let mut state = {
2020-05-08 20:42:31 +00:00
let mut memory = ui.memory();
let mut state = memory.collapsing_headers.entry(id).or_insert(State {
open: default_open,
..Default::default()
});
if interact.clicked {
state.open = !state.open;
2020-05-08 20:42:31 +00:00
state.toggle_time = ui.input().time;
}
*state
};
2020-05-08 20:42:31 +00:00
let where_to_put_background = ui.paint_list_len();
2020-05-08 20:42:31 +00:00
paint_icon(ui, &state, &interact);
2020-05-08 20:42:31 +00:00
ui.add_text(
text_pos,
label.text_style,
title,
2020-05-10 06:55:41 +00:00
Some(ui.style().interact(&interact).stroke_color),
);
2020-05-08 20:42:31 +00:00
ui.insert_paint_cmd(
where_to_put_background,
PaintCmd::Rect {
2020-05-10 06:55:41 +00:00
corner_radius: ui.style().interact(&interact).corner_radius,
fill_color: ui.style().interact(&interact).fill_color,
outline: ui.style().interact(&interact).outline,
rect: interact.rect,
},
);
2020-05-08 20:42:31 +00:00
ui.expand_to_include_child(interact.rect); // TODO: remove, just a test
2020-05-08 20:42:31 +00:00
let animation_time = ui.style().animation_time;
let time_since_toggle = (ui.input().time - state.toggle_time) as f32;
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
let animate = time_since_toggle < animation_time;
if animate {
2020-05-08 20:42:31 +00:00
ui.indent(id, |child_ui| {
let max_height = if state.open {
if let Some(full_height) = state.open_height {
remap(time_since_toggle, 0.0..=animation_time, 0.0..=full_height)
} else {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placehodler value that shows some movement:
10.0
}
} else {
2020-05-05 17:41:49 +00:00
let full_height = state.open_height.unwrap_or_default();
remap_clamp(time_since_toggle, 0.0..=animation_time, full_height..=0.0)
};
2020-05-08 20:42:31 +00:00
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = clip_rect.max.y.min(child_ui.rect().top() + max_height);
2020-05-08 20:42:31 +00:00
child_ui.set_clip_rect(clip_rect);
2020-05-08 20:42:31 +00:00
let top_left = child_ui.top_left();
add_contents(child_ui);
2020-05-05 06:15:20 +00:00
2020-05-08 20:42:31 +00:00
state.open_height = Some(child_ui.bounding_size().y);
2020-05-05 17:41:49 +00:00
2020-05-05 06:15:20 +00:00
// Pretend children took up less space:
2020-05-08 20:42:31 +00:00
let mut child_bounds = child_ui.child_bounds();
2020-05-05 06:15:20 +00:00
child_bounds.max.y = child_bounds.max.y.min(top_left.y + max_height);
2020-05-08 20:42:31 +00:00
child_ui.force_set_child_bounds(child_bounds);
});
} else if state.open {
2020-05-08 20:42:31 +00:00
let full_size = ui.indent(id, add_contents).rect.size();
2020-05-05 17:41:49 +00:00
state.open_height = Some(full_size.y);
}
2020-05-08 20:42:31 +00:00
ui.memory().collapsing_headers.insert(id, state);
ui.response(interact)
}
}
2020-05-08 20:42:31 +00:00
fn paint_icon(ui: &mut Ui, state: &State, interact: &InteractInfo) {
2020-05-10 06:55:41 +00:00
let stroke_color = ui.style().interact(interact).stroke_color;
let stroke_width = ui.style().interact(interact).stroke_width;
2020-05-08 20:42:31 +00:00
let (mut small_icon_rect, _) = ui.style().icon_rectangles(interact.rect);
small_icon_rect.set_center(pos2(
2020-05-08 20:42:31 +00:00
interact.rect.left() + ui.style().indent / 2.0,
interact.rect.center().y,
));
// Draw a minus:
2020-05-11 11:11:01 +00:00
ui.add_paint_cmd(PaintCmd::LineSegment {
points: [
pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.right(), small_icon_rect.center().y),
],
color: stroke_color,
width: stroke_width,
});
if !state.open {
// Draw it as a plus:
2020-05-11 11:11:01 +00:00
ui.add_paint_cmd(PaintCmd::LineSegment {
points: [
pos2(small_icon_rect.center().x, small_icon_rect.top()),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
],
color: stroke_color,
width: stroke_width,
});
}
}