1use std::hash::Hash;
2
3use crate::{
4 Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
5 TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
6 emath, epaint, pos2, remap, remap_clamp, vec2,
7};
8use emath::GuiRounding as _;
9use epaint::{Shape, StrokeKind};
10
11#[derive(Clone, Copy, Debug)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub(crate) struct InnerState {
14 open: bool,
15
16 #[cfg_attr(feature = "serde", serde(default))]
18 open_height: Option<f32>,
19}
20
21#[derive(Clone, Debug)]
27pub struct CollapsingState {
28 id: Id,
29 state: InnerState,
30}
31
32impl CollapsingState {
33 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
34 ctx.data_mut(|d| {
35 d.get_persisted::<InnerState>(id)
36 .map(|state| Self { id, state })
37 })
38 }
39
40 pub fn store(&self, ctx: &Context) {
41 ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
42 }
43
44 pub fn remove(&self, ctx: &Context) {
45 ctx.data_mut(|d| d.remove::<InnerState>(self.id));
46 }
47
48 pub fn id(&self) -> Id {
49 self.id
50 }
51
52 pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
53 Self::load(ctx, id).unwrap_or(Self {
54 id,
55 state: InnerState {
56 open: default_open,
57 open_height: None,
58 },
59 })
60 }
61
62 pub fn is_open(&self) -> bool {
63 self.state.open
64 }
65
66 pub fn set_open(&mut self, open: bool) {
67 self.state.open = open;
68 }
69
70 pub fn toggle(&mut self, ui: &Ui) {
71 self.state.open = !self.state.open;
72 ui.ctx().request_repaint();
73 }
74
75 pub fn openness(&self, ctx: &Context) -> f32 {
77 if ctx.memory(|mem| mem.everything_is_visible()) {
78 1.0
79 } else {
80 ctx.animate_bool_responsive(self.id, self.state.open)
81 }
82 }
83
84 pub(crate) fn show_default_button_with_size(
86 &mut self,
87 ui: &mut Ui,
88 button_size: Vec2,
89 ) -> Response {
90 let (_id, rect) = ui.allocate_space(button_size);
91 let response = ui.interact(rect, self.id, Sense::click());
92 response.widget_info(|| {
93 WidgetInfo::labeled(
94 WidgetType::Button,
95 ui.is_enabled(),
96 if self.is_open() { "Hide" } else { "Show" },
97 )
98 });
99
100 if response.clicked() {
101 self.toggle(ui);
102 }
103 let openness = self.openness(ui.ctx());
104 paint_default_icon(ui, openness, &response);
105 response
106 }
107
108 fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
110 self.show_button_indented(ui, paint_default_icon)
111 }
112
113 fn show_button_indented(
115 &mut self,
116 ui: &mut Ui,
117 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
118 ) -> Response {
119 let size = vec2(ui.spacing().indent, ui.spacing().icon_width);
120 let (_id, rect) = ui.allocate_space(size);
121 let response = ui.interact(rect, self.id, Sense::click());
122 if response.clicked() {
123 self.toggle(ui);
124 }
125
126 let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect);
127 icon_rect.set_center(pos2(
128 response.rect.left() + ui.spacing().indent / 2.0,
129 response.rect.center().y,
130 ));
131 let openness = self.openness(ui.ctx());
132 let small_icon_response = response.clone().with_new_rect(icon_rect);
133 icon_fn(ui, openness, &small_icon_response);
134 response
135 }
136
137 pub fn show_header<HeaderRet>(
156 mut self,
157 ui: &mut Ui,
158 add_header: impl FnOnce(&mut Ui) -> HeaderRet,
159 ) -> HeaderResponse<'_, HeaderRet> {
160 let header_response = ui.horizontal(|ui| {
161 let prev_item_spacing = ui.spacing_mut().item_spacing;
162 ui.spacing_mut().item_spacing.x = 0.0; let collapser = self.show_default_button_indented(ui);
164 ui.spacing_mut().item_spacing = prev_item_spacing;
165 (collapser, add_header(ui))
166 });
167 HeaderResponse {
168 state: self,
169 ui,
170 toggle_button_response: header_response.inner.0,
171 header_response: InnerResponse {
172 response: header_response.response,
173 inner: header_response.inner.1,
174 },
175 }
176 }
177
178 pub fn show_body_indented<R>(
183 &mut self,
184 header_response: &Response,
185 ui: &mut Ui,
186 add_body: impl FnOnce(&mut Ui) -> R,
187 ) -> Option<InnerResponse<R>> {
188 let id = self.id;
189 self.show_body_unindented(ui, |ui| {
190 ui.indent(id, |ui| {
191 ui.expand_to_include_x(header_response.rect.right());
193 add_body(ui)
194 })
195 .inner
196 })
197 }
198
199 pub fn show_body_unindented<R>(
202 &mut self,
203 ui: &mut Ui,
204 add_body: impl FnOnce(&mut Ui) -> R,
205 ) -> Option<InnerResponse<R>> {
206 let openness = self.openness(ui.ctx());
207
208 let builder = UiBuilder::new()
209 .ui_stack_info(UiStackInfo::new(UiKind::Collapsible))
210 .closable();
211
212 if openness <= 0.0 {
213 self.store(ui.ctx()); None
215 } else if openness < 1.0 {
216 Some(ui.scope_builder(builder, |child_ui| {
217 let max_height = if self.state.open && self.state.open_height.is_none() {
218 10.0
222 } else {
223 let full_height = self.state.open_height.unwrap_or_default();
224 remap_clamp(openness, 0.0..=1.0, 0.0..=full_height).round_ui()
225 };
226
227 let mut clip_rect = child_ui.clip_rect();
228 clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
229 child_ui.set_clip_rect(clip_rect);
230
231 let ret = add_body(child_ui);
232
233 let mut min_rect = child_ui.min_rect();
234 self.state.open_height = Some(min_rect.height());
235 if child_ui.should_close() {
236 self.state.open = false;
237 }
238 self.store(child_ui.ctx()); min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
242 child_ui.force_set_min_rect(min_rect);
243 ret
244 }))
245 } else {
246 let ret_response = ui.scope_builder(builder, add_body);
247 if ret_response.response.should_close() {
248 self.state.open = false;
249 }
250 let full_size = ret_response.response.rect.size();
251 self.state.open_height = Some(full_size.y);
252 self.store(ui.ctx()); Some(ret_response)
254 }
255 }
256
257 pub fn show_toggle_button(
281 &mut self,
282 ui: &mut Ui,
283 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
284 ) -> Response {
285 self.show_button_indented(ui, icon_fn)
286 }
287}
288
289#[must_use = "Remember to show the body"]
291pub struct HeaderResponse<'ui, HeaderRet> {
292 state: CollapsingState,
293 ui: &'ui mut Ui,
294 toggle_button_response: Response,
295 header_response: InnerResponse<HeaderRet>,
296}
297
298impl<HeaderRet> HeaderResponse<'_, HeaderRet> {
299 pub fn is_open(&self) -> bool {
300 self.state.is_open()
301 }
302
303 pub fn set_open(&mut self, open: bool) {
304 self.state.set_open(open);
305 }
306
307 pub fn toggle(&mut self) {
308 self.state.toggle(self.ui);
309 }
310
311 pub fn body<BodyRet>(
313 mut self,
314 add_body: impl FnOnce(&mut Ui) -> BodyRet,
315 ) -> (
316 Response,
317 InnerResponse<HeaderRet>,
318 Option<InnerResponse<BodyRet>>,
319 ) {
320 let body_response =
321 self.state
322 .show_body_indented(&self.header_response.response, self.ui, add_body);
323 (
324 self.toggle_button_response,
325 self.header_response,
326 body_response,
327 )
328 }
329
330 pub fn body_unindented<BodyRet>(
332 mut self,
333 add_body: impl FnOnce(&mut Ui) -> BodyRet,
334 ) -> (
335 Response,
336 InnerResponse<HeaderRet>,
337 Option<InnerResponse<BodyRet>>,
338 ) {
339 let body_response = self.state.show_body_unindented(self.ui, add_body);
340 (
341 self.toggle_button_response,
342 self.header_response,
343 body_response,
344 )
345 }
346}
347
348pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
352 let visuals = ui.style().interact(response);
353
354 let rect = response.rect;
355
356 let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
358 let rect = rect.expand(visuals.expansion);
359 let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
360 use std::f32::consts::TAU;
361 let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
362 for p in &mut points {
363 *p = rect.center() + rotation * (*p - rect.center());
364 }
365
366 ui.painter().add(Shape::convex_polygon(
367 points,
368 visuals.fg_stroke.color,
369 Stroke::NONE,
370 ));
371}
372
373pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
375
376#[must_use = "You should call .show()"]
392pub struct CollapsingHeader {
393 text: WidgetText,
394 default_open: bool,
395 open: Option<bool>,
396 id_salt: Id,
397 enabled: bool,
398 selectable: bool,
399 selected: bool,
400 show_background: bool,
401 icon: Option<IconPainter>,
402}
403
404impl CollapsingHeader {
405 pub fn new(text: impl Into<WidgetText>) -> Self {
412 let text = text.into();
413 let id_salt = Id::new(text.text());
414 Self {
415 text,
416 default_open: false,
417 open: None,
418 id_salt,
419 enabled: true,
420 selectable: false,
421 selected: false,
422 show_background: false,
423 icon: None,
424 }
425 }
426
427 #[inline]
430 pub fn default_open(mut self, open: bool) -> Self {
431 self.default_open = open;
432 self
433 }
434
435 #[inline]
441 pub fn open(mut self, open: Option<bool>) -> Self {
442 self.open = open;
443 self
444 }
445
446 #[inline]
449 pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
450 self.id_salt = Id::new(id_salt);
451 self
452 }
453
454 #[deprecated = "Renamed id_salt"]
457 #[inline]
458 pub fn id_source(mut self, id_salt: impl Hash) -> Self {
459 self.id_salt = Id::new(id_salt);
460 self
461 }
462
463 #[inline]
467 pub fn enabled(mut self, enabled: bool) -> Self {
468 self.enabled = enabled;
469 self
470 }
471
472 #[inline]
481 pub fn show_background(mut self, show_background: bool) -> Self {
482 self.show_background = show_background;
483 self
484 }
485
486 #[inline]
504 pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self {
505 self.icon = Some(Box::new(icon_fn));
506 self
507 }
508}
509
510struct Prepared {
511 header_response: Response,
512 state: CollapsingState,
513 openness: f32,
514}
515
516impl CollapsingHeader {
517 fn begin(self, ui: &mut Ui) -> Prepared {
518 assert!(
519 ui.layout().main_dir().is_vertical(),
520 "Horizontal collapsing is unimplemented"
521 );
522 let Self {
523 icon,
524 text,
525 default_open,
526 open,
527 id_salt,
528 enabled: _,
529 selectable,
530 selected,
531 show_background,
532 } = self;
533
534 let id = ui.make_persistent_id(id_salt);
537 let button_padding = ui.spacing().button_padding;
538
539 let available = ui.available_rect_before_wrap();
540 let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
541 let wrap_width = available.right() - text_pos.x;
542 let galley = text.into_galley(
543 ui,
544 Some(TextWrapMode::Extend),
545 wrap_width,
546 TextStyle::Button,
547 );
548 let text_max_x = text_pos.x + galley.size().x;
549
550 let mut desired_width = text_max_x + button_padding.x - available.left();
551 if ui.visuals().collapsing_header_frame {
552 desired_width = desired_width.max(available.width()); }
554
555 let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y);
556 desired_size = desired_size.at_least(ui.spacing().interact_size);
557 let (_, rect) = ui.allocate_space(desired_size);
558
559 let mut header_response = ui.interact(rect, id, Sense::click());
560 let text_pos = pos2(
561 text_pos.x,
562 header_response.rect.center().y - galley.size().y / 2.0,
563 );
564
565 let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
566 if let Some(open) = open {
567 if open != state.is_open() {
568 state.toggle(ui);
569 header_response.mark_changed();
570 }
571 } else if header_response.clicked() {
572 state.toggle(ui);
573 header_response.mark_changed();
574 }
575
576 header_response.widget_info(|| {
577 WidgetInfo::labeled(WidgetType::CollapsingHeader, ui.is_enabled(), galley.text())
578 });
579
580 let openness = state.openness(ui.ctx());
581
582 if ui.is_rect_visible(rect) {
583 let visuals = ui.style().interact_selectable(&header_response, selected);
584
585 if ui.visuals().collapsing_header_frame || show_background {
586 ui.painter().add(epaint::RectShape::new(
587 header_response.rect.expand(visuals.expansion),
588 visuals.corner_radius,
589 visuals.weak_bg_fill,
590 visuals.bg_stroke,
591 StrokeKind::Inside,
592 ));
593 }
594
595 if selected || selectable && (header_response.hovered() || header_response.has_focus())
596 {
597 let rect = rect.expand(visuals.expansion);
598
599 ui.painter().rect(
600 rect,
601 visuals.corner_radius,
602 visuals.bg_fill,
603 visuals.bg_stroke,
604 StrokeKind::Inside,
605 );
606 }
607
608 {
609 let (mut icon_rect, _) = ui.spacing().icon_rectangles(header_response.rect);
610 icon_rect.set_center(pos2(
611 header_response.rect.left() + ui.spacing().indent / 2.0,
612 header_response.rect.center().y,
613 ));
614 let icon_response = header_response.clone().with_new_rect(icon_rect);
615 if let Some(icon) = icon {
616 icon(ui, openness, &icon_response);
617 } else {
618 paint_default_icon(ui, openness, &icon_response);
619 }
620 }
621
622 ui.painter().galley(text_pos, galley, visuals.text_color());
623 }
624
625 Prepared {
626 header_response,
627 state,
628 openness,
629 }
630 }
631
632 #[inline]
633 pub fn show<R>(
634 self,
635 ui: &mut Ui,
636 add_body: impl FnOnce(&mut Ui) -> R,
637 ) -> CollapsingResponse<R> {
638 self.show_dyn(ui, Box::new(add_body), true)
639 }
640
641 #[inline]
642 pub fn show_unindented<R>(
643 self,
644 ui: &mut Ui,
645 add_body: impl FnOnce(&mut Ui) -> R,
646 ) -> CollapsingResponse<R> {
647 self.show_dyn(ui, Box::new(add_body), false)
648 }
649
650 fn show_dyn<'c, R>(
651 self,
652 ui: &mut Ui,
653 add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
654 indented: bool,
655 ) -> CollapsingResponse<R> {
656 ui.vertical(|ui| {
659 if !self.enabled {
660 ui.disable();
661 }
662
663 let Prepared {
664 header_response,
665 mut state,
666 openness,
667 } = self.begin(ui); let ret_response = if indented {
670 state.show_body_indented(&header_response, ui, add_body)
671 } else {
672 state.show_body_unindented(ui, add_body)
673 };
674
675 if let Some(ret_response) = ret_response {
676 CollapsingResponse {
677 header_response,
678 body_response: Some(ret_response.response),
679 body_returned: Some(ret_response.inner),
680 openness,
681 }
682 } else {
683 CollapsingResponse {
684 header_response,
685 body_response: None,
686 body_returned: None,
687 openness,
688 }
689 }
690 })
691 .inner
692 }
693}
694
695pub struct CollapsingResponse<R> {
697 pub header_response: Response,
699
700 pub body_response: Option<Response>,
702
703 pub body_returned: Option<R>,
705
706 pub openness: f32,
708}
709
710impl<R> CollapsingResponse<R> {
711 pub fn fully_closed(&self) -> bool {
713 self.openness <= 0.0
714 }
715
716 pub fn fully_open(&self) -> bool {
718 self.openness >= 1.0
719 }
720}