1use std::hash::Hash;
2
3use crate::*;
4use epaint::Shape;
5
6#[derive(Clone, Copy, Debug)]
7#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
8pub(crate) struct InnerState {
9 open: bool,
10
11 #[cfg_attr(feature = "serde", serde(default))]
13 open_height: Option<f32>,
14}
15
16#[derive(Clone, Debug)]
22pub struct CollapsingState {
23 id: Id,
24 state: InnerState,
25}
26
27impl CollapsingState {
28 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
29 ctx.data_mut(|d| {
30 d.get_persisted::<InnerState>(id)
31 .map(|state| Self { id, state })
32 })
33 }
34
35 pub fn store(&self, ctx: &Context) {
36 ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
37 }
38
39 pub fn remove(&self, ctx: &Context) {
40 ctx.data_mut(|d| d.remove::<InnerState>(self.id));
41 }
42
43 pub fn id(&self) -> Id {
44 self.id
45 }
46
47 pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
48 Self::load(ctx, id).unwrap_or(Self {
49 id,
50 state: InnerState {
51 open: default_open,
52 open_height: None,
53 },
54 })
55 }
56
57 pub fn is_open(&self) -> bool {
58 self.state.open
59 }
60
61 pub fn set_open(&mut self, open: bool) {
62 self.state.open = open;
63 }
64
65 pub fn toggle(&mut self, ui: &Ui) {
66 self.state.open = !self.state.open;
67 ui.ctx().request_repaint();
68 }
69
70 pub fn openness(&self, ctx: &Context) -> f32 {
72 if ctx.memory(|mem| mem.everything_is_visible()) {
73 1.0
74 } else {
75 ctx.animate_bool_responsive(self.id, self.state.open)
76 }
77 }
78
79 pub(crate) fn show_default_button_with_size(
81 &mut self,
82 ui: &mut Ui,
83 button_size: Vec2,
84 ) -> Response {
85 let (_id, rect) = ui.allocate_space(button_size);
86 let response = ui.interact(rect, self.id, Sense::click());
87 if response.clicked() {
88 self.toggle(ui);
89 }
90 let openness = self.openness(ui.ctx());
91 paint_default_icon(ui, openness, &response);
92 response
93 }
94
95 fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
97 self.show_button_indented(ui, paint_default_icon)
98 }
99
100 fn show_button_indented(
102 &mut self,
103 ui: &mut Ui,
104 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
105 ) -> Response {
106 let size = vec2(ui.spacing().indent, ui.spacing().icon_width);
107 let (_id, rect) = ui.allocate_space(size);
108 let response = ui.interact(rect, self.id, Sense::click());
109 if response.clicked() {
110 self.toggle(ui);
111 }
112
113 let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect);
114 icon_rect.set_center(pos2(
115 response.rect.left() + ui.spacing().indent / 2.0,
116 response.rect.center().y,
117 ));
118 let openness = self.openness(ui.ctx());
119 let small_icon_response = response.clone().with_new_rect(icon_rect);
120 icon_fn(ui, openness, &small_icon_response);
121 response
122 }
123
124 pub fn show_header<HeaderRet>(
143 mut self,
144 ui: &mut Ui,
145 add_header: impl FnOnce(&mut Ui) -> HeaderRet,
146 ) -> HeaderResponse<'_, HeaderRet> {
147 let header_response = ui.horizontal(|ui| {
148 let prev_item_spacing = ui.spacing_mut().item_spacing;
149 ui.spacing_mut().item_spacing.x = 0.0; let collapser = self.show_default_button_indented(ui);
151 ui.spacing_mut().item_spacing = prev_item_spacing;
152 (collapser, add_header(ui))
153 });
154 HeaderResponse {
155 state: self,
156 ui,
157 toggle_button_response: header_response.inner.0,
158 header_response: InnerResponse {
159 response: header_response.response,
160 inner: header_response.inner.1,
161 },
162 }
163 }
164
165 pub fn show_body_indented<R>(
170 &mut self,
171 header_response: &Response,
172 ui: &mut Ui,
173 add_body: impl FnOnce(&mut Ui) -> R,
174 ) -> Option<InnerResponse<R>> {
175 let id = self.id;
176 self.show_body_unindented(ui, |ui| {
177 ui.indent(id, |ui| {
178 ui.expand_to_include_x(header_response.rect.right());
180 add_body(ui)
181 })
182 .inner
183 })
184 }
185
186 pub fn show_body_unindented<R>(
189 &mut self,
190 ui: &mut Ui,
191 add_body: impl FnOnce(&mut Ui) -> R,
192 ) -> Option<InnerResponse<R>> {
193 let openness = self.openness(ui.ctx());
194 if openness <= 0.0 {
195 self.store(ui.ctx()); None
197 } else if openness < 1.0 {
198 Some(ui.scope(|child_ui| {
199 let max_height = if self.state.open && self.state.open_height.is_none() {
200 10.0
204 } else {
205 let full_height = self.state.open_height.unwrap_or_default();
206 remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
207 };
208
209 let mut clip_rect = child_ui.clip_rect();
210 clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
211 child_ui.set_clip_rect(clip_rect);
212
213 let ret = add_body(child_ui);
214
215 let mut min_rect = child_ui.min_rect();
216 self.state.open_height = Some(min_rect.height());
217 self.store(child_ui.ctx()); min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
221 child_ui.force_set_min_rect(min_rect);
222 ret
223 }))
224 } else {
225 let ret_response = ui.scope(add_body);
226 let full_size = ret_response.response.rect.size();
227 self.state.open_height = Some(full_size.y);
228 self.store(ui.ctx()); Some(ret_response)
230 }
231 }
232
233 pub fn show_toggle_button(
257 &mut self,
258 ui: &mut Ui,
259 icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
260 ) -> Response {
261 self.show_button_indented(ui, icon_fn)
262 }
263}
264
265#[must_use = "Remember to show the body"]
267pub struct HeaderResponse<'ui, HeaderRet> {
268 state: CollapsingState,
269 ui: &'ui mut Ui,
270 toggle_button_response: Response,
271 header_response: InnerResponse<HeaderRet>,
272}
273
274impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
275 pub fn is_open(&self) -> bool {
276 self.state.is_open()
277 }
278
279 pub fn set_open(&mut self, open: bool) {
280 self.state.set_open(open);
281 }
282
283 pub fn toggle(&mut self) {
284 self.state.toggle(self.ui);
285 }
286
287 pub fn body<BodyRet>(
289 mut self,
290 add_body: impl FnOnce(&mut Ui) -> BodyRet,
291 ) -> (
292 Response,
293 InnerResponse<HeaderRet>,
294 Option<InnerResponse<BodyRet>>,
295 ) {
296 let body_response =
297 self.state
298 .show_body_indented(&self.header_response.response, self.ui, add_body);
299 (
300 self.toggle_button_response,
301 self.header_response,
302 body_response,
303 )
304 }
305
306 pub fn body_unindented<BodyRet>(
308 mut self,
309 add_body: impl FnOnce(&mut Ui) -> BodyRet,
310 ) -> (
311 Response,
312 InnerResponse<HeaderRet>,
313 Option<InnerResponse<BodyRet>>,
314 ) {
315 let body_response = self.state.show_body_unindented(self.ui, add_body);
316 (
317 self.toggle_button_response,
318 self.header_response,
319 body_response,
320 )
321 }
322}
323
324pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
328 let visuals = ui.style().interact(response);
329
330 let rect = response.rect;
331
332 let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
334 let rect = rect.expand(visuals.expansion);
335 let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
336 use std::f32::consts::TAU;
337 let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
338 for p in &mut points {
339 *p = rect.center() + rotation * (*p - rect.center());
340 }
341
342 ui.painter().add(Shape::convex_polygon(
343 points,
344 visuals.fg_stroke.color,
345 Stroke::NONE,
346 ));
347}
348
349pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
351
352#[must_use = "You should call .show()"]
368pub struct CollapsingHeader {
369 text: WidgetText,
370 default_open: bool,
371 open: Option<bool>,
372 id_source: Id,
373 enabled: bool,
374 selectable: bool,
375 selected: bool,
376 show_background: bool,
377 icon: Option<IconPainter>,
378}
379
380impl CollapsingHeader {
381 pub fn new(text: impl Into<WidgetText>) -> Self {
388 let text = text.into();
389 let id_source = Id::new(text.text());
390 Self {
391 text,
392 default_open: false,
393 open: None,
394 id_source,
395 enabled: true,
396 selectable: false,
397 selected: false,
398 show_background: false,
399 icon: None,
400 }
401 }
402
403 #[inline]
406 pub fn default_open(mut self, open: bool) -> Self {
407 self.default_open = open;
408 self
409 }
410
411 #[inline]
417 pub fn open(mut self, open: Option<bool>) -> Self {
418 self.open = open;
419 self
420 }
421
422 #[inline]
425 pub fn id_source(mut self, id_source: impl Hash) -> Self {
426 self.id_source = Id::new(id_source);
427 self
428 }
429
430 #[inline]
434 pub fn enabled(mut self, enabled: bool) -> Self {
435 self.enabled = enabled;
436 self
437 }
438
439 #[inline]
448 pub fn show_background(mut self, show_background: bool) -> Self {
449 self.show_background = show_background;
450 self
451 }
452
453 #[inline]
471 pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self {
472 self.icon = Some(Box::new(icon_fn));
473 self
474 }
475}
476
477struct Prepared {
478 header_response: Response,
479 state: CollapsingState,
480 openness: f32,
481}
482
483impl CollapsingHeader {
484 fn begin(self, ui: &mut Ui) -> Prepared {
485 assert!(
486 ui.layout().main_dir().is_vertical(),
487 "Horizontal collapsing is unimplemented"
488 );
489 let Self {
490 icon,
491 text,
492 default_open,
493 open,
494 id_source,
495 enabled: _,
496 selectable,
497 selected,
498 show_background,
499 } = self;
500
501 let id = ui.make_persistent_id(id_source);
504 let button_padding = ui.spacing().button_padding;
505
506 let available = ui.available_rect_before_wrap();
507 let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
508 let wrap_width = available.right() - text_pos.x;
509 let galley = text.into_galley(
510 ui,
511 Some(TextWrapMode::Extend),
512 wrap_width,
513 TextStyle::Button,
514 );
515 let text_max_x = text_pos.x + galley.size().x;
516
517 let mut desired_width = text_max_x + button_padding.x - available.left();
518 if ui.visuals().collapsing_header_frame {
519 desired_width = desired_width.max(available.width()); }
521
522 let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y);
523 desired_size = desired_size.at_least(ui.spacing().interact_size);
524 let (_, rect) = ui.allocate_space(desired_size);
525
526 let mut header_response = ui.interact(rect, id, Sense::click());
527 let text_pos = pos2(
528 text_pos.x,
529 header_response.rect.center().y - galley.size().y / 2.0,
530 );
531
532 let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
533 if let Some(open) = open {
534 if open != state.is_open() {
535 state.toggle(ui);
536 header_response.mark_changed();
537 }
538 } else if header_response.clicked() {
539 state.toggle(ui);
540 header_response.mark_changed();
541 }
542
543 header_response.widget_info(|| {
544 WidgetInfo::labeled(WidgetType::CollapsingHeader, ui.is_enabled(), galley.text())
545 });
546
547 let openness = state.openness(ui.ctx());
548
549 if ui.is_rect_visible(rect) {
550 let visuals = ui.style().interact_selectable(&header_response, selected);
551
552 if ui.visuals().collapsing_header_frame || show_background {
553 ui.painter().add(epaint::RectShape::new(
554 header_response.rect.expand(visuals.expansion),
555 visuals.rounding,
556 visuals.weak_bg_fill,
557 visuals.bg_stroke,
558 ));
559 }
560
561 if selected || selectable && (header_response.hovered() || header_response.has_focus())
562 {
563 let rect = rect.expand(visuals.expansion);
564
565 ui.painter()
566 .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
567 }
568
569 {
570 let (mut icon_rect, _) = ui.spacing().icon_rectangles(header_response.rect);
571 icon_rect.set_center(pos2(
572 header_response.rect.left() + ui.spacing().indent / 2.0,
573 header_response.rect.center().y,
574 ));
575 let icon_response = header_response.clone().with_new_rect(icon_rect);
576 if let Some(icon) = icon {
577 icon(ui, openness, &icon_response);
578 } else {
579 paint_default_icon(ui, openness, &icon_response);
580 }
581 }
582
583 ui.painter().galley(text_pos, galley, visuals.text_color());
584 }
585
586 Prepared {
587 header_response,
588 state,
589 openness,
590 }
591 }
592
593 #[inline]
594 pub fn show<R>(
595 self,
596 ui: &mut Ui,
597 add_body: impl FnOnce(&mut Ui) -> R,
598 ) -> CollapsingResponse<R> {
599 self.show_dyn(ui, Box::new(add_body), true)
600 }
601
602 #[inline]
603 pub fn show_unindented<R>(
604 self,
605 ui: &mut Ui,
606 add_body: impl FnOnce(&mut Ui) -> R,
607 ) -> CollapsingResponse<R> {
608 self.show_dyn(ui, Box::new(add_body), false)
609 }
610
611 fn show_dyn<'c, R>(
612 self,
613 ui: &mut Ui,
614 add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
615 indented: bool,
616 ) -> CollapsingResponse<R> {
617 ui.vertical(|ui| {
620 if !self.enabled {
621 ui.disable();
622 }
623
624 let Prepared {
625 header_response,
626 mut state,
627 openness,
628 } = self.begin(ui); let ret_response = if indented {
631 state.show_body_indented(&header_response, ui, add_body)
632 } else {
633 state.show_body_unindented(ui, add_body)
634 };
635
636 if let Some(ret_response) = ret_response {
637 CollapsingResponse {
638 header_response,
639 body_response: Some(ret_response.response),
640 body_returned: Some(ret_response.inner),
641 openness,
642 }
643 } else {
644 CollapsingResponse {
645 header_response,
646 body_response: None,
647 body_returned: None,
648 openness,
649 }
650 }
651 })
652 .inner
653 }
654}
655
656pub struct CollapsingResponse<R> {
658 pub header_response: Response,
660
661 pub body_response: Option<Response>,
663
664 pub body_returned: Option<R>,
666
667 pub openness: f32,
669}
670
671impl<R> CollapsingResponse<R> {
672 pub fn fully_closed(&self) -> bool {
674 self.openness <= 0.0
675 }
676
677 pub fn fully_open(&self) -> bool {
679 self.openness >= 1.0
680 }
681}