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