egui_components/
collapsible.rs1use egui::{vec2, Id, Rect, Sense, Ui};
18use egui_components_theme::Theme;
19
20use crate::icon::{paint_icon, IconKind};
21
22pub struct Collapsible<'a> {
23 id: Id,
24 default_open: bool,
25 open: Option<&'a mut bool>,
26}
27
28impl<'a> Collapsible<'a> {
29 pub fn new(id_salt: impl std::hash::Hash) -> Self {
30 Self {
31 id: Id::new(id_salt),
32 default_open: false,
33 open: None,
34 }
35 }
36 pub fn default_open(mut self, open: bool) -> Self {
37 self.default_open = open;
38 self
39 }
40 pub fn open(mut self, open: &'a mut bool) -> Self {
41 self.open = Some(open);
42 self
43 }
44
45 pub fn show<R>(
46 mut self,
47 ui: &mut Ui,
48 header: impl FnOnce(&mut Ui),
49 body: impl FnOnce(&mut Ui) -> R,
50 ) -> egui::InnerResponse<Option<R>> {
51 let theme = Theme::get(ui.ctx());
52 let c = theme.colors;
53 let mem_id = ui.make_persistent_id(self.id);
54
55 let mut is_open = match &self.open {
56 Some(b) => **b,
57 None => ui
58 .data(|d| d.get_temp::<bool>(mem_id))
59 .unwrap_or(self.default_open),
60 };
61
62 let header_resp = ui
64 .horizontal(|ui| {
65 let chevron = 16.0;
66 let (rect, resp) = ui.allocate_exact_size(vec2(chevron, chevron), Sense::click());
67 let t = ui.ctx().animate_bool(mem_id.with("anim"), is_open);
68 let kind = if t > 0.5 {
69 IconKind::ChevronDown
70 } else {
71 IconKind::ChevronRight
72 };
73 let ir = Rect::from_center_size(rect.center(), vec2(chevron, chevron));
74 paint_icon(ui.painter(), kind, ir, c.muted_foreground, 1.6);
75 if resp.hovered() {
76 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
77 }
78 ui.add_space(4.0);
79 header(ui);
80 resp
81 })
82 .inner;
83
84 if header_resp.clicked() {
85 is_open = !is_open;
86 if let Some(b) = self.open.as_deref_mut() {
87 *b = is_open;
88 }
89 }
90 ui.data_mut(|d| d.insert_temp(mem_id, is_open));
91
92 let inner = if is_open {
93 let r = ui.scope(|ui| {
94 egui::Frame::new()
95 .inner_margin(egui::Margin {
96 left: 20,
97 right: 0,
98 top: 4,
99 bottom: 4,
100 })
101 .show(ui, |ui| body(ui))
102 .inner
103 });
104 Some(r.inner)
105 } else {
106 None
107 };
108
109 egui::InnerResponse::new(inner, header_resp)
110 }
111}