2020-05-08 15:16:32 +00:00
|
|
|
use crate::{layout::Direction, widgets::Label, *};
|
2020-04-22 16:15:27 +00:00
|
|
|
|
2020-05-07 08:47:03 +00:00
|
|
|
#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
|
2020-05-02 09:37:12 +00:00
|
|
|
#[serde(default)]
|
2020-04-22 16:15:27 +00:00
|
|
|
pub(crate) struct State {
|
2020-05-02 09:37:12 +00:00
|
|
|
open: bool,
|
2020-05-05 01:05:36 +00:00
|
|
|
|
2020-05-02 09:37:12 +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>,
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for State {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
open: false,
|
2020-05-02 09:37:12 +00:00
|
|
|
toggle_time: -f64::INFINITY,
|
2020-05-05 17:41:49 +00:00
|
|
|
open_height: None,
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct CollapsingHeader {
|
2020-05-08 15:16:32 +00:00
|
|
|
label: Label,
|
2020-04-22 16:15:27 +00:00
|
|
|
default_open: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CollapsingHeader {
|
2020-05-08 15:16:32 +00:00
|
|
|
pub fn new(label: impl Into<String>) -> Self {
|
2020-04-22 16:15:27 +00:00
|
|
|
Self {
|
2020-05-08 15:16:32 +00:00
|
|
|
label: Label::new(label)
|
|
|
|
.text_style(TextStyle::Button)
|
|
|
|
.multiline(false),
|
2020-04-22 16:15:27 +00:00
|
|
|
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 {
|
2020-04-22 16:15:27 +00:00
|
|
|
assert!(
|
2020-05-13 19:36:15 +00:00
|
|
|
ui.layout().dir() == Direction::Vertical,
|
2020-04-22 16:15:27 +00:00
|
|
|
"Horizontal collapsing is unimplemented"
|
|
|
|
);
|
|
|
|
let Self {
|
2020-05-08 15:16:32 +00:00
|
|
|
label,
|
2020-04-22 16:15:27 +00:00
|
|
|
default_open,
|
|
|
|
} = self;
|
|
|
|
|
2020-05-10 16:59:18 +00:00
|
|
|
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
|
2020-05-08 15:16:32 +00:00
|
|
|
|
2020-05-12 20:21:04 +00:00
|
|
|
let title = label.text();
|
2020-05-08 20:42:31 +00:00
|
|
|
let id = ui.make_unique_id(title);
|
2020-05-12 20:21:04 +00:00
|
|
|
|
|
|
|
let available = ui.available_finite();
|
|
|
|
let text_pos = available.min + vec2(ui.style().indent, 0.0);
|
2020-05-16 09:27:02 +00:00
|
|
|
let galley = label.layout(available.width() - ui.style().indent, ui);
|
|
|
|
let text_max_x = text_pos.x + galley.size.x;
|
2020-05-17 07:44:09 +00:00
|
|
|
let desired_width = text_max_x - available.left();
|
|
|
|
let desired_width = desired_width.max(available.width());
|
2020-04-22 22:17:37 +00:00
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
let interact = ui.reserve_space(
|
2020-04-22 16:15:27 +00:00
|
|
|
vec2(
|
2020-05-08 15:16:32 +00:00
|
|
|
desired_width,
|
2020-05-16 09:27:02 +00:00
|
|
|
galley.size.y + 2.0 * ui.style().button_padding.y,
|
2020-04-22 16:15:27 +00:00
|
|
|
),
|
|
|
|
Some(id),
|
|
|
|
);
|
2020-05-16 09:27:02 +00:00
|
|
|
let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0);
|
2020-04-22 16:15:27 +00:00
|
|
|
|
2020-05-05 17:41:49 +00:00
|
|
|
let mut state = {
|
2020-05-08 20:42:31 +00:00
|
|
|
let mut memory = ui.memory();
|
2020-04-22 16:15:27 +00:00
|
|
|
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;
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|
|
|
|
*state
|
|
|
|
};
|
|
|
|
|
2020-05-17 07:44:09 +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 openness = if state.open {
|
|
|
|
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
|
|
|
|
} else {
|
|
|
|
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
|
|
|
|
};
|
|
|
|
let animate = time_since_toggle < animation_time;
|
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
let where_to_put_background = ui.paint_list_len();
|
2020-04-22 16:15:27 +00:00
|
|
|
|
2020-05-17 07:44:09 +00:00
|
|
|
paint_icon(ui, &interact, openness);
|
2020-04-22 16:15:27 +00:00
|
|
|
|
2020-05-16 16:17:35 +00:00
|
|
|
ui.add_galley(
|
2020-05-08 15:16:32 +00:00
|
|
|
text_pos,
|
2020-05-16 16:17:35 +00:00
|
|
|
galley,
|
2020-05-08 15:16:32 +00:00
|
|
|
label.text_style,
|
2020-05-10 06:55:41 +00:00
|
|
|
Some(ui.style().interact(&interact).stroke_color),
|
2020-04-22 16:15:27 +00:00
|
|
|
);
|
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.insert_paint_cmd(
|
2020-05-08 15:16:32 +00:00
|
|
|
where_to_put_background,
|
|
|
|
PaintCmd::Rect {
|
2020-05-10 06:55:41 +00:00
|
|
|
corner_radius: ui.style().interact(&interact).corner_radius,
|
2020-05-17 07:44:09 +00:00
|
|
|
fill_color: ui.style().interact(&interact).bg_fill_color,
|
|
|
|
outline: None,
|
2020-05-08 15:16:32 +00:00
|
|
|
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 15:16:32 +00:00
|
|
|
|
2020-04-26 20:30:24 +00:00
|
|
|
if animate {
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.indent(id, |child_ui| {
|
2020-04-22 16:15:27 +00:00
|
|
|
let max_height = if state.open {
|
2020-05-12 16:21:09 +00:00
|
|
|
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
|
|
|
|
}
|
2020-04-22 16:15:27 +00:00
|
|
|
} 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-04-22 16:15:27 +00:00
|
|
|
};
|
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
let mut clip_rect = child_ui.clip_rect();
|
2020-05-12 16:21:09 +00:00
|
|
|
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-04-22 16:15:27 +00:00
|
|
|
|
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);
|
2020-04-22 16:15:27 +00:00
|
|
|
});
|
|
|
|
} 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-04-22 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.memory().collapsing_headers.insert(id, state);
|
|
|
|
ui.response(interact)
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-17 07:44:09 +00:00
|
|
|
fn paint_icon(ui: &mut Ui, interact: &InteractInfo, openness: f32) {
|
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-04-22 16:15:27 +00:00
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
let (mut small_icon_rect, _) = ui.style().icon_rectangles(interact.rect);
|
2020-04-22 22:17:37 +00:00
|
|
|
small_icon_rect.set_center(pos2(
|
2020-05-08 20:42:31 +00:00
|
|
|
interact.rect.left() + ui.style().indent / 2.0,
|
2020-04-22 22:17:37 +00:00
|
|
|
interact.rect.center().y,
|
|
|
|
));
|
|
|
|
|
2020-05-17 07:44:09 +00:00
|
|
|
// Draw a pointy triangle arrow:
|
|
|
|
let rect = Rect::from_center_size(
|
|
|
|
small_icon_rect.center(),
|
|
|
|
vec2(small_icon_rect.width(), small_icon_rect.height()) * 0.75,
|
|
|
|
);
|
|
|
|
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()];
|
|
|
|
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
|
|
|
|
for p in &mut points {
|
|
|
|
let v = *p - rect.center();
|
|
|
|
let v = rotation.rotate_other(v);
|
|
|
|
*p = rect.center() + v;
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|
2020-05-17 07:44:09 +00:00
|
|
|
// }
|
|
|
|
|
|
|
|
ui.add_paint_cmd(PaintCmd::Path {
|
|
|
|
path: mesher::Path::from_point_loop(&points),
|
|
|
|
closed: true,
|
|
|
|
fill_color: None,
|
|
|
|
outline: Some(Outline::new(stroke_width, stroke_color)),
|
|
|
|
});
|
2020-04-22 16:15:27 +00:00
|
|
|
}
|