1use std::hash::Hash;
2
3use crate::{
4 emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
5 Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
6};
7use epaint::Shape;
8
9#[derive(Clone, Copy, Debug)]
10#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
11pub(crate) struct InnerState {
12 open: bool,
13
14 #[cfg_attr(feature = "serde", serde(default))]
16 open_height: Option<f32>,
17}
18
19#[derive(Clone, Debug)]
25pub struct CollapsingState {
26 id: Id,
27 state: InnerState,
28}
29
30impl CollapsingState {
31 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
32 ctx.data_mut(|d| {
33 d.get_persisted::<InnerState>(id)
34 .map(|state| Self { id, state })
35 })
36 }
37
38 pub fn store(&self, ctx: &Context) {
39 ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
40 }
41
42 pub fn remove(&self, ctx: &Context) {
43 ctx.data_mut(|d| d.remove::<InnerState>(self.id));
44 }
45
46 pub fn id(&self) -> Id {
47 self.id
48 }
49
50 pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
51 Self::load(ctx, id).unwrap_or(Self {
52 id,
53 state: InnerState {
54 open: default_open,
55 open_height: None,
56 },
57 })
58 }
59
60 pub fn is_open(&self) -> bool {
61 self.state.open
62 }
63
64 pub fn set_open(&mut self, open: bool) {
65 self.state.open = open;
66 }
67
68 pub fn toggle(&mut self, ui: &Ui) {
69 self.state.open = !self.state.open;
70 ui.ctx().request_repaint();
71 }
72
73 pub fn openness(&self, ctx: &Context) -> f32 {
75 if ctx.memory(|mem| mem.everything_is_visible()) {
76 1.0
77 } else {
78 ctx.animate_bool_responsive(self.id, self.state.open)
79 }
80 }
81
82 pub(crate) fn show_default_button_with_size(
84 &mut self,
85 ui: &mut Ui,
86 button_size: Vec2,
87 ) -> Response {
88 let (_id, rect) = ui.allocate_space(button_size);
89 let response = ui.interact(rect, self.id, Sense::click());
90 response.widget_info(|| {
91 WidgetInfo::labeled(
92 WidgetType::Button,
93 ui.is_enabled(),
94 if self.is_open() { "Hide" } else { "Show" },
95 )
96 });
97
98 if response.clicked() {
99 self.toggle(ui);
100 }
101 let openness = self.openness(ui.ctx());
102 paint_default_icon(ui, openness, &response);
103 response
104 }
105
106 fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
108 self.show_button_indented(ui, paint_default_icon)
109 }
110
111 fn show_button_indented(
113 &mut self,
114 ui: &mut Ui,
115 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
116 ) -> Response {
117 let size = vec2(ui.spacing().indent, ui.spacing().icon_width);
118 let (_id, rect) = ui.allocate_space(size);
119 let response = ui.interact(rect, self.id, Sense::click());
120 if response.clicked() {
121 self.toggle(ui);
122 }
123
124 let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect);
125 icon_rect.set_center(pos2(
126 response.rect.left() + ui.spacing().indent / 2.0,
127 response.rect.center().y,
128 ));
129 let openness = self.openness(ui.ctx());
130 let small_icon_response = response.clone().with_new_rect(icon_rect);
131 icon_fn(ui, openness, &small_icon_response);
132 response
133 }
134
135 pub fn show_header<HeaderRet>(
154 mut self,
155 ui: &mut Ui,
156 add_header: impl FnOnce(&mut Ui) -> HeaderRet,
157 ) -> HeaderResponse<'_, HeaderRet> {
158 let header_response = ui.horizontal(|ui| {
159 let prev_item_spacing = ui.spacing_mut().item_spacing;
160 ui.spacing_mut().item_spacing.x = 0.0; let collapser = self.show_default_button_indented(ui);
162 ui.spacing_mut().item_spacing = prev_item_spacing;
163 (collapser, add_header(ui))
164 });
165 HeaderResponse {
166 state: self,
167 ui,
168 toggle_button_response: header_response.inner.0,
169 header_response: InnerResponse {
170 response: header_response.response,
171 inner: header_response.inner.1,
172 },
173 }
174 }
175
176 pub fn show_body_indented<R>(
181 &mut self,
182 header_response: &Response,
183 ui: &mut Ui,
184 add_body: impl FnOnce(&mut Ui) -> R,
185 ) -> Option<InnerResponse<R>> {
186 let id = self.id;
187 self.show_body_unindented(ui, |ui| {
188 ui.indent(id, |ui| {
189 ui.expand_to_include_x(header_response.rect.right());
191 add_body(ui)
192 })
193 .inner
194 })
195 }
196
197 pub fn show_body_unindented<R>(
200 &mut self,
201 ui: &mut Ui,
202 add_body: impl FnOnce(&mut Ui) -> R,
203 ) -> Option<InnerResponse<R>> {
204 let openness = self.openness(ui.ctx());
205 if openness <= 0.0 {
206 self.store(ui.ctx()); None
208 } else if openness < 1.0 {
209 Some(ui.scope(|child_ui| {
210 let max_height = if self.state.open && self.state.open_height.is_none() {
211 10.0
215 } else {
216 let full_height = self.state.open_height.unwrap_or_default();
217 remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
218 };
219
220 let mut clip_rect = child_ui.clip_rect();
221 clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
222 child_ui.set_clip_rect(clip_rect);
223
224 let ret = add_body(child_ui);
225
226 let mut min_rect = child_ui.min_rect();
227 self.state.open_height = Some(min_rect.height());
228 self.store(child_ui.ctx()); min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
232 child_ui.force_set_min_rect(min_rect);
233 ret
234 }))
235 } else {
236 let ret_response = ui.scope(add_body);
237 let full_size = ret_response.response.rect.size();
238 self.state.open_height = Some(full_size.y);
239 self.store(ui.ctx()); Some(ret_response)
241 }
242 }
243
244 pub fn show_toggle_button(
268 &mut self,
269 ui: &mut Ui,
270 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
271 ) -> Response {
272 self.show_button_indented(ui, icon_fn)
273 }
274}
275
276#[must_use = "Remember to show the body"]
278pub struct HeaderResponse<'ui, HeaderRet> {
279 state: CollapsingState,
280 ui: &'ui mut Ui,
281 toggle_button_response: Response,
282 header_response: InnerResponse<HeaderRet>,
283}
284
285impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
286 pub fn is_open(&self) -> bool {
287 self.state.is_open()
288 }
289
290 pub fn set_open(&mut self, open: bool) {
291 self.state.set_open(open);
292 }
293
294 pub fn toggle(&mut self) {
295 self.state.toggle(self.ui);
296 }
297
298 pub fn body<BodyRet>(
300 mut self,
301 add_body: impl FnOnce(&mut Ui) -> BodyRet,
302 ) -> (
303 Response,
304 InnerResponse<HeaderRet>,
305 Option<InnerResponse<BodyRet>>,
306 ) {
307 let body_response =
308 self.state
309 .show_body_indented(&self.header_response.response, self.ui, add_body);
310 (
311 self.toggle_button_response,
312 self.header_response,
313 body_response,
314 )
315 }
316
317 pub fn body_unindented<BodyRet>(
319 mut self,
320 add_body: impl FnOnce(&mut Ui) -> BodyRet,
321 ) -> (
322 Response,
323 InnerResponse<HeaderRet>,
324 Option<InnerResponse<BodyRet>>,
325 ) {
326 let body_response = self.state.show_body_unindented(self.ui, add_body);
327 (
328 self.toggle_button_response,
329 self.header_response,
330 body_response,
331 )
332 }
333}
334
335pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
339 let visuals = ui.style().interact(response);
340
341 let rect = response.rect;
342
343 let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
345 let rect = rect.expand(visuals.expansion);
346 let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
347 use std::f32::consts::TAU;
348 let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
349 for p in &mut points {
350 *p = rect.center() + rotation * (*p - rect.center());
351 }
352
353 ui.painter().add(Shape::convex_polygon(
354 points,
355 visuals.fg_stroke.color,
356 Stroke::NONE,
357 ));
358}
359
360pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
362
363#[must_use = "You should call .show()"]
379pub struct CollapsingHeader {
380 text: WidgetText,
381 default_open: bool,
382 open: Option<bool>,
383 id_salt: Id,
384 enabled: bool,
385 selectable: bool,
386 selected: bool,
387 show_background: bool,
388 icon: Option<IconPainter>,
389}
390
391impl CollapsingHeader {
392 pub fn new(text: impl Into<WidgetText>) -> Self {
399 let text = text.into();
400 let id_salt = Id::new(text.text());
401 Self {
402 text,
403 default_open: false,
404 open: None,
405 id_salt,
406 enabled: true,
407 selectable: false,
408 selected: false,
409 show_background: false,
410 icon: None,
411 }
412 }
413
414 #[inline]
417 pub fn default_open(mut self, open: bool) -> Self {
418 self.default_open = open;
419 self
420 }
421
422 #[inline]
428 pub fn open(mut self, open: Option<bool>) -> Self {
429 self.open = open;
430 self
431 }
432
433 #[inline]
436 pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
437 self.id_salt = Id::new(id_salt);
438 self
439 }
440
441 #[deprecated = "Renamed id_salt"]
444 #[inline]
445 pub fn id_source(mut self, id_salt: impl Hash) -> Self {
446 self.id_salt = Id::new(id_salt);
447 self
448 }
449
450 #[inline]
454 pub fn enabled(mut self, enabled: bool) -> Self {
455 self.enabled = enabled;
456 self
457 }
458
459 #[inline]
468 pub fn show_background(mut self, show_background: bool) -> Self {
469 self.show_background = show_background;
470 self
471 }
472
473 #[inline]
491 pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self {
492 self.icon = Some(Box::new(icon_fn));
493 self
494 }
495}
496
497struct Prepared {
498 header_response: Response,
499 state: CollapsingState,
500 openness: f32,
501}
502
503impl CollapsingHeader {
504 fn begin(self, ui: &mut Ui) -> Prepared {
505 assert!(
506 ui.layout().main_dir().is_vertical(),
507 "Horizontal collapsing is unimplemented"
508 );
509 let Self {
510 icon,
511 text,
512 default_open,
513 open,
514 id_salt,
515 enabled: _,
516 selectable,
517 selected,
518 show_background,
519 } = self;
520
521 let id = ui.make_persistent_id(id_salt);
524 let button_padding = ui.spacing().button_padding;
525
526 let available = ui.available_rect_before_wrap();
527 let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
528 let wrap_width = available.right() - text_pos.x;
529 let galley = text.into_galley(
530 ui,
531 Some(TextWrapMode::Extend),
532 wrap_width,
533 TextStyle::Button,
534 );
535 let text_max_x = text_pos.x + galley.size().x;
536
537 let mut desired_width = text_max_x + button_padding.x - available.left();
538 if ui.visuals().collapsing_header_frame {
539 desired_width = desired_width.max(available.width()); }
541
542 let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y);
543 desired_size = desired_size.at_least(ui.spacing().interact_size);
544 let (_, rect) = ui.allocate_space(desired_size);
545
546 let mut header_response = ui.interact(rect, id, Sense::click());
547 let text_pos = pos2(
548 text_pos.x,
549 header_response.rect.center().y - galley.size().y / 2.0,
550 );
551
552 let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
553 if let Some(open) = open {
554 if open != state.is_open() {
555 state.toggle(ui);
556 header_response.mark_changed();
557 }
558 } else if header_response.clicked() {
559 state.toggle(ui);
560 header_response.mark_changed();
561 }
562
563 header_response.widget_info(|| {
564 WidgetInfo::labeled(WidgetType::CollapsingHeader, ui.is_enabled(), galley.text())
565 });
566
567 let openness = state.openness(ui.ctx());
568
569 if ui.is_rect_visible(rect) {
570 let visuals = ui.style().interact_selectable(&header_response, selected);
571
572 if ui.visuals().collapsing_header_frame || show_background {
573 ui.painter().add(epaint::RectShape::new(
574 header_response.rect.expand(visuals.expansion),
575 visuals.rounding,
576 visuals.weak_bg_fill,
577 visuals.bg_stroke,
578 ));
579 }
580
581 if selected || selectable && (header_response.hovered() || header_response.has_focus())
582 {
583 let rect = rect.expand(visuals.expansion);
584
585 ui.painter()
586 .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
587 }
588
589 {
590 let (mut icon_rect, _) = ui.spacing().icon_rectangles(header_response.rect);
591 icon_rect.set_center(pos2(
592 header_response.rect.left() + ui.spacing().indent / 2.0,
593 header_response.rect.center().y,
594 ));
595 let icon_response = header_response.clone().with_new_rect(icon_rect);
596 if let Some(icon) = icon {
597 icon(ui, openness, &icon_response);
598 } else {
599 paint_default_icon(ui, openness, &icon_response);
600 }
601 }
602
603 ui.painter().galley(text_pos, galley, visuals.text_color());
604 }
605
606 Prepared {
607 header_response,
608 state,
609 openness,
610 }
611 }
612
613 #[inline]
614 pub fn show<R>(
615 self,
616 ui: &mut Ui,
617 add_body: impl FnOnce(&mut Ui) -> R,
618 ) -> CollapsingResponse<R> {
619 self.show_dyn(ui, Box::new(add_body), true)
620 }
621
622 #[inline]
623 pub fn show_unindented<R>(
624 self,
625 ui: &mut Ui,
626 add_body: impl FnOnce(&mut Ui) -> R,
627 ) -> CollapsingResponse<R> {
628 self.show_dyn(ui, Box::new(add_body), false)
629 }
630
631 fn show_dyn<'c, R>(
632 self,
633 ui: &mut Ui,
634 add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
635 indented: bool,
636 ) -> CollapsingResponse<R> {
637 ui.vertical(|ui| {
640 if !self.enabled {
641 ui.disable();
642 }
643
644 let Prepared {
645 header_response,
646 mut state,
647 openness,
648 } = self.begin(ui); let ret_response = if indented {
651 state.show_body_indented(&header_response, ui, add_body)
652 } else {
653 state.show_body_unindented(ui, add_body)
654 };
655
656 if let Some(ret_response) = ret_response {
657 CollapsingResponse {
658 header_response,
659 body_response: Some(ret_response.response),
660 body_returned: Some(ret_response.inner),
661 openness,
662 }
663 } else {
664 CollapsingResponse {
665 header_response,
666 body_response: None,
667 body_returned: None,
668 openness,
669 }
670 }
671 })
672 .inner
673 }
674}
675
676pub struct CollapsingResponse<R> {
678 pub header_response: Response,
680
681 pub body_response: Option<Response>,
683
684 pub body_returned: Option<R>,
686
687 pub openness: f32,
689}
690
691impl<R> CollapsingResponse<R> {
692 pub fn fully_closed(&self) -> bool {
694 self.openness <= 0.0
695 }
696
697 pub fn fully_open(&self) -> bool {
699 self.openness >= 1.0
700 }
701}