1use emath::GuiRounding as _;
19
20use crate::{
21 Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef,
22 Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp,
23 vec2,
24};
25
26fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
27 ctx.animate_bool_responsive(id, is_expanded)
28}
29
30#[derive(Clone, Copy, Debug)]
32#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
33pub struct PanelState {
34 pub rect: Rect,
35}
36
37impl PanelState {
38 pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
39 ctx.data_mut(|d| d.get_persisted(bar_id))
40 }
41
42 pub fn size(&self) -> Vec2 {
44 self.rect.size()
45 }
46
47 fn store(self, ctx: &Context, bar_id: Id) {
48 ctx.data_mut(|d| d.insert_persisted(bar_id, self));
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum Side {
57 Left,
58 Right,
59}
60
61impl Side {
62 fn opposite(self) -> Self {
63 match self {
64 Self::Left => Self::Right,
65 Self::Right => Self::Left,
66 }
67 }
68
69 fn set_rect_width(self, rect: &mut Rect, width: f32) {
70 match self {
71 Self::Left => rect.max.x = rect.min.x + width,
72 Self::Right => rect.min.x = rect.max.x - width,
73 }
74 }
75
76 fn side_x(self, rect: Rect) -> f32 {
77 match self {
78 Self::Left => rect.left(),
79 Self::Right => rect.right(),
80 }
81 }
82
83 fn sign(self) -> f32 {
84 match self {
85 Self::Left => -1.0,
86 Self::Right => 1.0,
87 }
88 }
89}
90
91#[must_use = "You should call .show()"]
110pub struct SidePanel {
111 side: Side,
112 id: Id,
113 frame: Option<Frame>,
114 resizable: bool,
115 show_separator_line: bool,
116 default_width: f32,
117 width_range: Rangef,
118}
119
120impl SidePanel {
121 pub fn left(id: impl Into<Id>) -> Self {
123 Self::new(Side::Left, id)
124 }
125
126 pub fn right(id: impl Into<Id>) -> Self {
128 Self::new(Side::Right, id)
129 }
130
131 pub fn new(side: Side, id: impl Into<Id>) -> Self {
133 Self {
134 side,
135 id: id.into(),
136 frame: None,
137 resizable: true,
138 show_separator_line: true,
139 default_width: 200.0,
140 width_range: Rangef::new(96.0, f32::INFINITY),
141 }
142 }
143
144 #[inline]
159 pub fn resizable(mut self, resizable: bool) -> Self {
160 self.resizable = resizable;
161 self
162 }
163
164 #[inline]
168 pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
169 self.show_separator_line = show_separator_line;
170 self
171 }
172
173 #[inline]
175 pub fn default_width(mut self, default_width: f32) -> Self {
176 self.default_width = default_width;
177 self.width_range = Rangef::new(
178 self.width_range.min.at_most(default_width),
179 self.width_range.max.at_least(default_width),
180 );
181 self
182 }
183
184 #[inline]
186 pub fn min_width(mut self, min_width: f32) -> Self {
187 self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
188 self
189 }
190
191 #[inline]
193 pub fn max_width(mut self, max_width: f32) -> Self {
194 self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
195 self
196 }
197
198 #[inline]
200 pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
201 let width_range = width_range.into();
202 self.default_width = clamp_to_range(self.default_width, width_range);
203 self.width_range = width_range;
204 self
205 }
206
207 #[inline]
209 pub fn exact_width(mut self, width: f32) -> Self {
210 self.default_width = width;
211 self.width_range = Rangef::point(width);
212 self
213 }
214
215 #[inline]
217 pub fn frame(mut self, frame: Frame) -> Self {
218 self.frame = Some(frame);
219 self
220 }
221}
222
223impl SidePanel {
224 pub fn show_inside<R>(
226 self,
227 ui: &mut Ui,
228 add_contents: impl FnOnce(&mut Ui) -> R,
229 ) -> InnerResponse<R> {
230 self.show_inside_dyn(ui, Box::new(add_contents))
231 }
232
233 fn show_inside_dyn<'c, R>(
235 self,
236 ui: &mut Ui,
237 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
238 ) -> InnerResponse<R> {
239 let Self {
240 side,
241 id,
242 frame,
243 resizable,
244 show_separator_line,
245 default_width,
246 width_range,
247 } = self;
248
249 let available_rect = ui.available_rect_before_wrap();
250 let mut panel_rect = available_rect;
251 let mut width = default_width;
252 {
253 if let Some(state) = PanelState::load(ui.ctx(), id) {
254 width = state.rect.width();
255 }
256 width = clamp_to_range(width, width_range).at_most(available_rect.width());
257 side.set_rect_width(&mut panel_rect, width);
258 ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
259 }
260
261 let resize_id = id.with("__resize");
262 let mut resize_hover = false;
263 let mut is_resizing = false;
264 if resizable {
265 if let Some(resize_response) = ui.ctx().read_response(resize_id) {
267 resize_hover = resize_response.hovered();
268 is_resizing = resize_response.dragged();
269
270 if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() {
271 width = (pointer.x - side.side_x(panel_rect)).abs();
272 width = clamp_to_range(width, width_range).at_most(available_rect.width());
273 side.set_rect_width(&mut panel_rect, width);
274 }
275 }
276 }
277
278 panel_rect = panel_rect.round_ui();
279
280 let mut panel_ui = ui.new_child(
281 UiBuilder::new()
282 .id_salt(id)
283 .ui_stack_info(UiStackInfo::new(match side {
284 Side::Left => UiKind::LeftPanel,
285 Side::Right => UiKind::RightPanel,
286 }))
287 .max_rect(panel_rect)
288 .layout(Layout::top_down(Align::Min)),
289 );
290 panel_ui.expand_to_include_rect(panel_rect);
291 panel_ui.set_clip_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
294 let inner_response = frame.show(&mut panel_ui, |ui| {
295 ui.set_min_height(ui.max_rect().height()); ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0));
297 add_contents(ui)
298 });
299
300 let rect = inner_response.response.rect;
301
302 {
303 let mut cursor = ui.cursor();
304 match side {
305 Side::Left => {
306 cursor.min.x = rect.max.x;
307 }
308 Side::Right => {
309 cursor.max.x = rect.min.x;
310 }
311 }
312 ui.set_cursor(cursor);
313 }
314 ui.expand_to_include_rect(rect);
315
316 if resizable {
317 let resize_x = side.opposite().side_x(panel_rect);
321 let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range())
322 .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0));
323 let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
324 resize_hover = resize_response.hovered();
325 is_resizing = resize_response.dragged();
326 }
327
328 if resize_hover || is_resizing {
329 let cursor_icon = if width <= width_range.min {
330 match self.side {
331 Side::Left => CursorIcon::ResizeEast,
332 Side::Right => CursorIcon::ResizeWest,
333 }
334 } else if width < width_range.max {
335 CursorIcon::ResizeHorizontal
336 } else {
337 match self.side {
338 Side::Left => CursorIcon::ResizeWest,
339 Side::Right => CursorIcon::ResizeEast,
340 }
341 };
342 ui.ctx().set_cursor_icon(cursor_icon);
343 }
344
345 PanelState { rect }.store(ui.ctx(), id);
346
347 {
348 let stroke = if is_resizing {
349 ui.style().visuals.widgets.active.fg_stroke } else if resize_hover {
351 ui.style().visuals.widgets.hovered.fg_stroke } else if show_separator_line {
353 ui.style().visuals.widgets.noninteractive.bg_stroke } else {
356 Stroke::NONE
357 };
358 let resize_x = side.opposite().side_x(rect);
360
361 let resize_x = resize_x + 0.5 * side.sign() * stroke.width;
363 ui.painter().vline(resize_x, panel_rect.y_range(), stroke);
364 }
365
366 inner_response
367 }
368
369 pub fn show<R>(
371 self,
372 ctx: &Context,
373 add_contents: impl FnOnce(&mut Ui) -> R,
374 ) -> InnerResponse<R> {
375 self.show_dyn(ctx, Box::new(add_contents))
376 }
377
378 fn show_dyn<'c, R>(
380 self,
381 ctx: &Context,
382 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
383 ) -> InnerResponse<R> {
384 let side = self.side;
385 let available_rect = ctx.available_rect();
386 let mut panel_ui = Ui::new(
387 ctx.clone(),
388 self.id,
389 UiBuilder::new()
390 .layer_id(LayerId::background())
391 .max_rect(available_rect),
392 );
393 panel_ui.set_clip_rect(ctx.content_rect());
394 panel_ui
395 .response()
396 .widget_info(|| WidgetInfo::new(WidgetType::Panel));
397
398 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
399 let rect = inner_response.response.rect;
400
401 match side {
402 Side::Left => ctx.pass_state_mut(|state| {
403 state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
404 }),
405 Side::Right => ctx.pass_state_mut(|state| {
406 state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
407 }),
408 }
409 inner_response
410 }
411
412 pub fn show_animated<R>(
415 self,
416 ctx: &Context,
417 is_expanded: bool,
418 add_contents: impl FnOnce(&mut Ui) -> R,
419 ) -> Option<InnerResponse<R>> {
420 let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
421
422 if 0.0 == how_expanded {
423 None
424 } else if how_expanded < 1.0 {
425 let expanded_width = PanelState::load(ctx, self.id)
429 .map_or(self.default_width, |state| state.rect.width());
430 let fake_width = how_expanded * expanded_width;
431 Self {
432 id: self.id.with("animating_panel"),
433 ..self
434 }
435 .resizable(false)
436 .exact_width(fake_width)
437 .show(ctx, |_ui| {});
438 None
439 } else {
440 Some(self.show(ctx, add_contents))
442 }
443 }
444
445 pub fn show_animated_inside<R>(
448 self,
449 ui: &mut Ui,
450 is_expanded: bool,
451 add_contents: impl FnOnce(&mut Ui) -> R,
452 ) -> Option<InnerResponse<R>> {
453 let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
454
455 if 0.0 == how_expanded {
456 None
457 } else if how_expanded < 1.0 {
458 let expanded_width = PanelState::load(ui.ctx(), self.id)
462 .map_or(self.default_width, |state| state.rect.width());
463 let fake_width = how_expanded * expanded_width;
464 Self {
465 id: self.id.with("animating_panel"),
466 ..self
467 }
468 .resizable(false)
469 .exact_width(fake_width)
470 .show_inside(ui, |_ui| {});
471 None
472 } else {
473 Some(self.show_inside(ui, add_contents))
475 }
476 }
477
478 pub fn show_animated_between<R>(
480 ctx: &Context,
481 is_expanded: bool,
482 collapsed_panel: Self,
483 expanded_panel: Self,
484 add_contents: impl FnOnce(&mut Ui, f32) -> R,
485 ) -> Option<InnerResponse<R>> {
486 let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
487
488 if 0.0 == how_expanded {
489 Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
490 } else if how_expanded < 1.0 {
491 let collapsed_width = PanelState::load(ctx, collapsed_panel.id)
493 .map_or(collapsed_panel.default_width, |state| state.rect.width());
494 let expanded_width = PanelState::load(ctx, expanded_panel.id)
495 .map_or(expanded_panel.default_width, |state| state.rect.width());
496 let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
497 Self {
498 id: expanded_panel.id.with("animating_panel"),
499 ..expanded_panel
500 }
501 .resizable(false)
502 .exact_width(fake_width)
503 .show(ctx, |ui| add_contents(ui, how_expanded));
504 None
505 } else {
506 Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
507 }
508 }
509
510 pub fn show_animated_between_inside<R>(
512 ui: &mut Ui,
513 is_expanded: bool,
514 collapsed_panel: Self,
515 expanded_panel: Self,
516 add_contents: impl FnOnce(&mut Ui, f32) -> R,
517 ) -> InnerResponse<R> {
518 let how_expanded =
519 animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
520
521 if 0.0 == how_expanded {
522 collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
523 } else if how_expanded < 1.0 {
524 let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id)
526 .map_or(collapsed_panel.default_width, |state| state.rect.width());
527 let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id)
528 .map_or(expanded_panel.default_width, |state| state.rect.width());
529 let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
530 Self {
531 id: expanded_panel.id.with("animating_panel"),
532 ..expanded_panel
533 }
534 .resizable(false)
535 .exact_width(fake_width)
536 .show_inside(ui, |ui| add_contents(ui, how_expanded))
537 } else {
538 expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
539 }
540 }
541}
542
543#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub enum TopBottomSide {
548 Top,
549 Bottom,
550}
551
552impl TopBottomSide {
553 fn opposite(self) -> Self {
554 match self {
555 Self::Top => Self::Bottom,
556 Self::Bottom => Self::Top,
557 }
558 }
559
560 fn set_rect_height(self, rect: &mut Rect, height: f32) {
561 match self {
562 Self::Top => rect.max.y = rect.min.y + height,
563 Self::Bottom => rect.min.y = rect.max.y - height,
564 }
565 }
566
567 fn side_y(self, rect: Rect) -> f32 {
568 match self {
569 Self::Top => rect.top(),
570 Self::Bottom => rect.bottom(),
571 }
572 }
573
574 fn sign(self) -> f32 {
575 match self {
576 Self::Top => -1.0,
577 Self::Bottom => 1.0,
578 }
579 }
580}
581
582#[must_use = "You should call .show()"]
601pub struct TopBottomPanel {
602 side: TopBottomSide,
603 id: Id,
604 frame: Option<Frame>,
605 resizable: bool,
606 show_separator_line: bool,
607 default_height: Option<f32>,
608 height_range: Rangef,
609}
610
611impl TopBottomPanel {
612 pub fn top(id: impl Into<Id>) -> Self {
614 Self::new(TopBottomSide::Top, id)
615 }
616
617 pub fn bottom(id: impl Into<Id>) -> Self {
619 Self::new(TopBottomSide::Bottom, id)
620 }
621
622 pub fn new(side: TopBottomSide, id: impl Into<Id>) -> Self {
624 Self {
625 side,
626 id: id.into(),
627 frame: None,
628 resizable: false,
629 show_separator_line: true,
630 default_height: None,
631 height_range: Rangef::new(20.0, f32::INFINITY),
632 }
633 }
634
635 #[inline]
650 pub fn resizable(mut self, resizable: bool) -> Self {
651 self.resizable = resizable;
652 self
653 }
654
655 #[inline]
659 pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
660 self.show_separator_line = show_separator_line;
661 self
662 }
663
664 #[inline]
667 pub fn default_height(mut self, default_height: f32) -> Self {
668 self.default_height = Some(default_height);
669 self.height_range = Rangef::new(
670 self.height_range.min.at_most(default_height),
671 self.height_range.max.at_least(default_height),
672 );
673 self
674 }
675
676 #[inline]
678 pub fn min_height(mut self, min_height: f32) -> Self {
679 self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
680 self
681 }
682
683 #[inline]
685 pub fn max_height(mut self, max_height: f32) -> Self {
686 self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
687 self
688 }
689
690 #[inline]
692 pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
693 let height_range = height_range.into();
694 self.default_height = self
695 .default_height
696 .map(|default_height| clamp_to_range(default_height, height_range));
697 self.height_range = height_range;
698 self
699 }
700
701 #[inline]
703 pub fn exact_height(mut self, height: f32) -> Self {
704 self.default_height = Some(height);
705 self.height_range = Rangef::point(height);
706 self
707 }
708
709 #[inline]
711 pub fn frame(mut self, frame: Frame) -> Self {
712 self.frame = Some(frame);
713 self
714 }
715}
716
717impl TopBottomPanel {
718 pub fn show_inside<R>(
720 self,
721 ui: &mut Ui,
722 add_contents: impl FnOnce(&mut Ui) -> R,
723 ) -> InnerResponse<R> {
724 self.show_inside_dyn(ui, Box::new(add_contents))
725 }
726
727 fn show_inside_dyn<'c, R>(
729 self,
730 ui: &mut Ui,
731 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
732 ) -> InnerResponse<R> {
733 let Self {
734 side,
735 id,
736 frame,
737 resizable,
738 show_separator_line,
739 default_height,
740 height_range,
741 } = self;
742
743 let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
744
745 let available_rect = ui.available_rect_before_wrap();
746 let mut panel_rect = available_rect;
747
748 let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
749 state.rect.height()
750 } else {
751 default_height
752 .unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y)
753 };
754 {
755 height = clamp_to_range(height, height_range).at_most(available_rect.height());
756 side.set_rect_height(&mut panel_rect, height);
757 ui.ctx()
758 .check_for_id_clash(id, panel_rect, "TopBottomPanel");
759 }
760
761 let resize_id = id.with("__resize");
762 let mut resize_hover = false;
763 let mut is_resizing = false;
764 if resizable {
765 if let Some(resize_response) = ui.ctx().read_response(resize_id) {
767 resize_hover = resize_response.hovered();
768 is_resizing = resize_response.dragged();
769
770 if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() {
771 height = (pointer.y - side.side_y(panel_rect)).abs();
772 height = clamp_to_range(height, height_range).at_most(available_rect.height());
773 side.set_rect_height(&mut panel_rect, height);
774 }
775 }
776 }
777
778 panel_rect = panel_rect.round_ui();
779
780 let mut panel_ui = ui.new_child(
781 UiBuilder::new()
782 .id_salt(id)
783 .ui_stack_info(UiStackInfo::new(match side {
784 TopBottomSide::Top => UiKind::TopPanel,
785 TopBottomSide::Bottom => UiKind::BottomPanel,
786 }))
787 .max_rect(panel_rect)
788 .layout(Layout::top_down(Align::Min)),
789 );
790 panel_ui.expand_to_include_rect(panel_rect);
791 panel_ui.set_clip_rect(panel_rect); let inner_response = frame.show(&mut panel_ui, |ui| {
794 ui.set_min_width(ui.max_rect().width()); ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0));
796 add_contents(ui)
797 });
798
799 let rect = inner_response.response.rect;
800
801 {
802 let mut cursor = ui.cursor();
803 match side {
804 TopBottomSide::Top => {
805 cursor.min.y = rect.max.y;
806 }
807 TopBottomSide::Bottom => {
808 cursor.max.y = rect.min.y;
809 }
810 }
811 ui.set_cursor(cursor);
812 }
813 ui.expand_to_include_rect(rect);
814
815 if resizable {
816 let resize_y = side.opposite().side_y(panel_rect);
821 let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y)
822 .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));
823 let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
824 resize_hover = resize_response.hovered();
825 is_resizing = resize_response.dragged();
826 }
827
828 if resize_hover || is_resizing {
829 let cursor_icon = if height <= height_range.min {
830 match self.side {
831 TopBottomSide::Top => CursorIcon::ResizeSouth,
832 TopBottomSide::Bottom => CursorIcon::ResizeNorth,
833 }
834 } else if height < height_range.max {
835 CursorIcon::ResizeVertical
836 } else {
837 match self.side {
838 TopBottomSide::Top => CursorIcon::ResizeNorth,
839 TopBottomSide::Bottom => CursorIcon::ResizeSouth,
840 }
841 };
842 ui.ctx().set_cursor_icon(cursor_icon);
843 }
844
845 PanelState { rect }.store(ui.ctx(), id);
846
847 {
848 let stroke = if is_resizing {
849 ui.style().visuals.widgets.active.fg_stroke } else if resize_hover {
851 ui.style().visuals.widgets.hovered.fg_stroke } else if show_separator_line {
853 ui.style().visuals.widgets.noninteractive.bg_stroke } else {
856 Stroke::NONE
857 };
858 let resize_y = side.opposite().side_y(rect);
860
861 let resize_y = resize_y + 0.5 * side.sign() * stroke.width;
863 ui.painter().hline(panel_rect.x_range(), resize_y, stroke);
864 }
865
866 inner_response
867 }
868
869 pub fn show<R>(
871 self,
872 ctx: &Context,
873 add_contents: impl FnOnce(&mut Ui) -> R,
874 ) -> InnerResponse<R> {
875 self.show_dyn(ctx, Box::new(add_contents))
876 }
877
878 fn show_dyn<'c, R>(
880 self,
881 ctx: &Context,
882 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
883 ) -> InnerResponse<R> {
884 let available_rect = ctx.available_rect();
885 let side = self.side;
886
887 let mut panel_ui = Ui::new(
888 ctx.clone(),
889 self.id,
890 UiBuilder::new()
891 .layer_id(LayerId::background())
892 .max_rect(available_rect),
893 );
894 panel_ui.set_clip_rect(ctx.content_rect());
895
896 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
897 let rect = inner_response.response.rect;
898
899 match side {
900 TopBottomSide::Top => {
901 ctx.pass_state_mut(|state| {
902 state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
903 });
904 }
905 TopBottomSide::Bottom => {
906 ctx.pass_state_mut(|state| {
907 state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
908 });
909 }
910 }
911
912 inner_response
913 }
914
915 pub fn show_animated<R>(
918 self,
919 ctx: &Context,
920 is_expanded: bool,
921 add_contents: impl FnOnce(&mut Ui) -> R,
922 ) -> Option<InnerResponse<R>> {
923 let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
924
925 if 0.0 == how_expanded {
926 None
927 } else if how_expanded < 1.0 {
928 let expanded_height = PanelState::load(ctx, self.id)
932 .map(|state| state.rect.height())
933 .or(self.default_height)
934 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
935 let fake_height = how_expanded * expanded_height;
936 Self {
937 id: self.id.with("animating_panel"),
938 ..self
939 }
940 .resizable(false)
941 .exact_height(fake_height)
942 .show(ctx, |_ui| {});
943 None
944 } else {
945 Some(self.show(ctx, add_contents))
947 }
948 }
949
950 pub fn show_animated_inside<R>(
953 self,
954 ui: &mut Ui,
955 is_expanded: bool,
956 add_contents: impl FnOnce(&mut Ui) -> R,
957 ) -> Option<InnerResponse<R>> {
958 let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
959
960 if 0.0 == how_expanded {
961 None
962 } else if how_expanded < 1.0 {
963 let expanded_height = PanelState::load(ui.ctx(), self.id)
967 .map(|state| state.rect.height())
968 .or(self.default_height)
969 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
970 let fake_height = how_expanded * expanded_height;
971 Self {
972 id: self.id.with("animating_panel"),
973 ..self
974 }
975 .resizable(false)
976 .exact_height(fake_height)
977 .show_inside(ui, |_ui| {});
978 None
979 } else {
980 Some(self.show_inside(ui, add_contents))
982 }
983 }
984
985 pub fn show_animated_between<R>(
987 ctx: &Context,
988 is_expanded: bool,
989 collapsed_panel: Self,
990 expanded_panel: Self,
991 add_contents: impl FnOnce(&mut Ui, f32) -> R,
992 ) -> Option<InnerResponse<R>> {
993 let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
994
995 if 0.0 == how_expanded {
996 Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
997 } else if how_expanded < 1.0 {
998 let collapsed_height = PanelState::load(ctx, collapsed_panel.id)
1000 .map(|state| state.rect.height())
1001 .or(collapsed_panel.default_height)
1002 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
1003
1004 let expanded_height = PanelState::load(ctx, expanded_panel.id)
1005 .map(|state| state.rect.height())
1006 .or(expanded_panel.default_height)
1007 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
1008
1009 let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
1010 Self {
1011 id: expanded_panel.id.with("animating_panel"),
1012 ..expanded_panel
1013 }
1014 .resizable(false)
1015 .exact_height(fake_height)
1016 .show(ctx, |ui| add_contents(ui, how_expanded));
1017 None
1018 } else {
1019 Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
1020 }
1021 }
1022
1023 pub fn show_animated_between_inside<R>(
1025 ui: &mut Ui,
1026 is_expanded: bool,
1027 collapsed_panel: Self,
1028 expanded_panel: Self,
1029 add_contents: impl FnOnce(&mut Ui, f32) -> R,
1030 ) -> InnerResponse<R> {
1031 let how_expanded =
1032 animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
1033
1034 if 0.0 == how_expanded {
1035 collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1036 } else if how_expanded < 1.0 {
1037 let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id)
1039 .map(|state| state.rect.height())
1040 .or(collapsed_panel.default_height)
1041 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1042
1043 let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id)
1044 .map(|state| state.rect.height())
1045 .or(expanded_panel.default_height)
1046 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1047
1048 let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
1049 Self {
1050 id: expanded_panel.id.with("animating_panel"),
1051 ..expanded_panel
1052 }
1053 .resizable(false)
1054 .exact_height(fake_height)
1055 .show_inside(ui, |ui| add_contents(ui, how_expanded))
1056 } else {
1057 expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1058 }
1059 }
1060}
1061
1062#[must_use = "You should call .show()"]
1087#[derive(Default)]
1088pub struct CentralPanel {
1089 frame: Option<Frame>,
1090}
1091
1092impl CentralPanel {
1093 #[inline]
1095 pub fn frame(mut self, frame: Frame) -> Self {
1096 self.frame = Some(frame);
1097 self
1098 }
1099}
1100
1101impl CentralPanel {
1102 pub fn show_inside<R>(
1104 self,
1105 ui: &mut Ui,
1106 add_contents: impl FnOnce(&mut Ui) -> R,
1107 ) -> InnerResponse<R> {
1108 self.show_inside_dyn(ui, Box::new(add_contents))
1109 }
1110
1111 fn show_inside_dyn<'c, R>(
1113 self,
1114 ui: &mut Ui,
1115 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1116 ) -> InnerResponse<R> {
1117 let Self { frame } = self;
1118
1119 let panel_rect = ui.available_rect_before_wrap();
1120 let mut panel_ui = ui.new_child(
1121 UiBuilder::new()
1122 .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
1123 .max_rect(panel_rect)
1124 .layout(Layout::top_down(Align::Min)),
1125 );
1126 panel_ui.set_clip_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
1129 frame.show(&mut panel_ui, |ui| {
1130 ui.expand_to_include_rect(ui.max_rect()); add_contents(ui)
1132 })
1133 }
1134
1135 pub fn show<R>(
1137 self,
1138 ctx: &Context,
1139 add_contents: impl FnOnce(&mut Ui) -> R,
1140 ) -> InnerResponse<R> {
1141 self.show_dyn(ctx, Box::new(add_contents))
1142 }
1143
1144 fn show_dyn<'c, R>(
1146 self,
1147 ctx: &Context,
1148 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1149 ) -> InnerResponse<R> {
1150 let id = Id::new((ctx.viewport_id(), "central_panel"));
1151
1152 let mut panel_ui = Ui::new(
1153 ctx.clone(),
1154 id,
1155 UiBuilder::new()
1156 .layer_id(LayerId::background())
1157 .max_rect(ctx.available_rect().round_ui()),
1158 );
1159 panel_ui.set_clip_rect(ctx.content_rect());
1160
1161 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
1162
1163 ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
1165
1166 inner_response
1167 }
1168}
1169
1170fn clamp_to_range(x: f32, range: Rangef) -> f32 {
1171 let range = range.as_positive();
1172 x.clamp(range.min, range.max)
1173}