1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! `Collapsible` — the low-level show/hide primitive behind
//! [`Accordion`](crate::accordion::Accordion).
//!
//! Unlike `Accordion` (which owns a styled title bar), `Collapsible` lets you
//! render any trigger content next to the toggle chevron and any body when
//! open. Open state lives in egui memory keyed by the id, or pass
//! [`open`](Collapsible::open) for controlled state.
//!
//! ```ignore
//! sc::Collapsible::new("adv").show(
//! ui,
//! |ui| { ui.add(sc::Label::new("Advanced settings").strong()); },
//! |ui| { ui.add(sc::Switch::new(&mut flag)); },
//! );
//! ```
use egui::{vec2, Id, Rect, Sense, Ui};
use egui_components_theme::Theme;
use crate::icon::{paint_icon, IconKind};
pub struct Collapsible<'a> {
id: Id,
default_open: bool,
open: Option<&'a mut bool>,
}
impl<'a> Collapsible<'a> {
pub fn new(id_salt: impl std::hash::Hash) -> Self {
Self {
id: Id::new(id_salt),
default_open: false,
open: None,
}
}
pub fn default_open(mut self, open: bool) -> Self {
self.default_open = open;
self
}
pub fn open(mut self, open: &'a mut bool) -> Self {
self.open = Some(open);
self
}
pub fn show<R>(
mut self,
ui: &mut Ui,
header: impl FnOnce(&mut Ui),
body: impl FnOnce(&mut Ui) -> R,
) -> egui::InnerResponse<Option<R>> {
let theme = Theme::get(ui.ctx());
let c = theme.colors;
let mem_id = ui.make_persistent_id(self.id);
let mut is_open = match &self.open {
Some(b) => **b,
None => ui
.data(|d| d.get_temp::<bool>(mem_id))
.unwrap_or(self.default_open),
};
// Header row: clickable chevron + caller content.
let header_resp = ui
.horizontal(|ui| {
let chevron = 16.0;
let (rect, resp) = ui.allocate_exact_size(vec2(chevron, chevron), Sense::click());
let t = ui.ctx().animate_bool(mem_id.with("anim"), is_open);
let kind = if t > 0.5 {
IconKind::ChevronDown
} else {
IconKind::ChevronRight
};
let ir = Rect::from_center_size(rect.center(), vec2(chevron, chevron));
paint_icon(ui.painter(), kind, ir, c.muted_foreground, 1.6);
if resp.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
ui.add_space(4.0);
header(ui);
resp
})
.inner;
if header_resp.clicked() {
is_open = !is_open;
if let Some(b) = self.open.as_deref_mut() {
*b = is_open;
}
}
ui.data_mut(|d| d.insert_temp(mem_id, is_open));
let inner = if is_open {
let r = ui.scope(|ui| {
egui::Frame::new()
.inner_margin(egui::Margin {
left: 20,
right: 0,
top: 4,
bottom: 4,
})
.show(ui, |ui| body(ui))
.inner
});
Some(r.inner)
} else {
None
};
egui::InnerResponse::new(inner, header_resp)
}
}