use egui::{
pos2, Color32, Id, InnerResponse, Pos2, Response, Sense, Stroke, Ui, Vec2, WidgetInfo,
WidgetText, WidgetType,
};
use crate::theme::Theme;
#[must_use = "Call `.show(ui, |ui| ...)` to render."]
pub struct CollapsingSection<'a> {
id_salt: Id,
label: WidgetText,
open: Option<&'a mut bool>,
default_open: bool,
}
impl<'a> std::fmt::Debug for CollapsingSection<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CollapsingSection")
.field("id_salt", &self.id_salt)
.field("label", &self.label.text())
.field("open", &self.open.as_deref().copied())
.field("default_open", &self.default_open)
.finish()
}
}
impl<'a> CollapsingSection<'a> {
pub fn new(id_salt: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
Self {
id_salt: Id::new(("elegance_collapsing", id_salt)),
label: label.into(),
open: None,
default_open: false,
}
}
pub fn open(mut self, open: &'a mut bool) -> Self {
self.open = Some(open);
self
}
pub fn default_open(mut self, open: bool) -> Self {
self.default_open = open;
self
}
pub fn show<R>(
self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let theme = Theme::current(ui.ctx());
let mut is_open = match self.open.as_deref() {
Some(flag) => *flag,
None => ui.ctx().data(|d| {
d.get_temp::<bool>(self.id_salt)
.unwrap_or(self.default_open)
}),
};
let trigger = trigger_row(ui, self.label.text(), &theme, is_open);
let label_text = self.label.text().to_string();
trigger.widget_info(|| {
WidgetInfo::selected(WidgetType::CollapsingHeader, true, is_open, &label_text)
});
if trigger.clicked() {
is_open = !is_open;
}
match self.open {
Some(flag) => *flag = is_open,
None => {
ui.ctx().data_mut(|d| d.insert_temp(self.id_salt, is_open));
}
}
let inner = if is_open { Some(add_body(ui)) } else { None };
InnerResponse::new(inner, trigger)
}
}
fn trigger_row(ui: &mut Ui, label: &str, theme: &Theme, open: bool) -> Response {
let p = &theme.palette;
let t = &theme.typography;
const PAD_X: f32 = 4.0;
const PAD_Y: f32 = 4.0;
const CHEVRON: f32 = 12.0;
const GAP: f32 = 8.0;
let galley = crate::theme::placeholder_galley(ui, label, t.label, false, f32::INFINITY);
let content_w = CHEVRON + GAP + galley.size().x;
let desired = Vec2::new(content_w + PAD_X * 2.0, galley.size().y + PAD_Y * 2.0);
let (rect, resp) = ui.allocate_exact_size(desired, Sense::click());
if ui.is_rect_visible(rect) {
let hovered = resp.hovered();
let label_color = if hovered { p.text } else { p.text_muted };
let chevron_color = if hovered { p.sky } else { p.text_muted };
let chev_center = pos2(rect.min.x + PAD_X + CHEVRON * 0.5, rect.center().y);
draw_chevron(ui, chev_center, CHEVRON, chevron_color, open);
let text_pos = pos2(
rect.min.x + PAD_X + CHEVRON + GAP,
rect.center().y - galley.size().y * 0.5,
);
ui.painter().galley(text_pos, galley, label_color);
}
resp
}
fn draw_chevron(ui: &mut Ui, center: Pos2, size: f32, color: Color32, open: bool) {
let half = size * 0.3;
let points: Vec<Pos2> = if open {
vec![
pos2(center.x - half, center.y - half * 0.55),
pos2(center.x + half, center.y - half * 0.55),
pos2(center.x, center.y + half * 0.75),
]
} else {
vec![
pos2(center.x - half * 0.55, center.y - half),
pos2(center.x - half * 0.55, center.y + half),
pos2(center.x + half * 0.75, center.y),
]
};
ui.painter()
.add(egui::Shape::convex_polygon(points, color, Stroke::NONE));
}