1use crate::{
19 lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt,
20 Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
21};
22
23fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
24 ctx.animate_bool_responsive(id, is_expanded)
25}
26
27#[derive(Clone, Copy, Debug)]
29#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
30pub struct PanelState {
31 pub rect: Rect,
32}
33
34impl PanelState {
35 pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
36 ctx.data_mut(|d| d.get_persisted(bar_id))
37 }
38
39 pub fn size(&self) -> Vec2 {
41 self.rect.size()
42 }
43
44 fn store(self, ctx: &Context, bar_id: Id) {
45 ctx.data_mut(|d| d.insert_persisted(bar_id, self));
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum Side {
54 Left,
55 Right,
56}
57
58impl Side {
59 fn opposite(self) -> Self {
60 match self {
61 Self::Left => Self::Right,
62 Self::Right => Self::Left,
63 }
64 }
65
66 fn set_rect_width(self, rect: &mut Rect, width: f32) {
67 match self {
68 Self::Left => rect.max.x = rect.min.x + width,
69 Self::Right => rect.min.x = rect.max.x - width,
70 }
71 }
72
73 fn side_x(self, rect: Rect) -> f32 {
74 match self {
75 Self::Left => rect.left(),
76 Self::Right => rect.right(),
77 }
78 }
79}
80
81#[must_use = "You should call .show()"]
100pub struct SidePanel {
101 side: Side,
102 id: Id,
103 frame: Option<Frame>,
104 resizable: bool,
105 show_separator_line: bool,
106 default_width: f32,
107 width_range: Rangef,
108}
109
110impl SidePanel {
111 pub fn left(id: impl Into<Id>) -> Self {
113 Self::new(Side::Left, id)
114 }
115
116 pub fn right(id: impl Into<Id>) -> Self {
118 Self::new(Side::Right, id)
119 }
120
121 pub fn new(side: Side, id: impl Into<Id>) -> Self {
123 Self {
124 side,
125 id: id.into(),
126 frame: None,
127 resizable: true,
128 show_separator_line: true,
129 default_width: 200.0,
130 width_range: Rangef::new(96.0, f32::INFINITY),
131 }
132 }
133
134 #[inline]
146 pub fn resizable(mut self, resizable: bool) -> Self {
147 self.resizable = resizable;
148 self
149 }
150
151 #[inline]
155 pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
156 self.show_separator_line = show_separator_line;
157 self
158 }
159
160 #[inline]
162 pub fn default_width(mut self, default_width: f32) -> Self {
163 self.default_width = default_width;
164 self.width_range = Rangef::new(
165 self.width_range.min.at_most(default_width),
166 self.width_range.max.at_least(default_width),
167 );
168 self
169 }
170
171 #[inline]
173 pub fn min_width(mut self, min_width: f32) -> Self {
174 self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
175 self
176 }
177
178 #[inline]
180 pub fn max_width(mut self, max_width: f32) -> Self {
181 self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
182 self
183 }
184
185 #[inline]
187 pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
188 let width_range = width_range.into();
189 self.default_width = clamp_to_range(self.default_width, width_range);
190 self.width_range = width_range;
191 self
192 }
193
194 #[inline]
196 pub fn exact_width(mut self, width: f32) -> Self {
197 self.default_width = width;
198 self.width_range = Rangef::point(width);
199 self
200 }
201
202 #[inline]
204 pub fn frame(mut self, frame: Frame) -> Self {
205 self.frame = Some(frame);
206 self
207 }
208}
209
210impl SidePanel {
211 pub fn show_inside<R>(
213 self,
214 ui: &mut Ui,
215 add_contents: impl FnOnce(&mut Ui) -> R,
216 ) -> InnerResponse<R> {
217 self.show_inside_dyn(ui, Box::new(add_contents))
218 }
219
220 fn show_inside_dyn<'c, R>(
222 self,
223 ui: &mut Ui,
224 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
225 ) -> InnerResponse<R> {
226 let Self {
227 side,
228 id,
229 frame,
230 resizable,
231 show_separator_line,
232 default_width,
233 width_range,
234 } = self;
235
236 let available_rect = ui.available_rect_before_wrap();
237 let mut panel_rect = available_rect;
238 let mut width = default_width;
239 {
240 if let Some(state) = PanelState::load(ui.ctx(), id) {
241 width = state.rect.width();
242 }
243 width = clamp_to_range(width, width_range).at_most(available_rect.width());
244 side.set_rect_width(&mut panel_rect, width);
245 ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
246 }
247
248 let resize_id = id.with("__resize");
249 let mut resize_hover = false;
250 let mut is_resizing = false;
251 if resizable {
252 if let Some(resize_response) = ui.ctx().read_response(resize_id) {
254 resize_hover = resize_response.hovered();
255 is_resizing = resize_response.dragged();
256
257 if is_resizing {
258 if let Some(pointer) = resize_response.interact_pointer_pos() {
259 width = (pointer.x - side.side_x(panel_rect)).abs();
260 width = clamp_to_range(width, width_range).at_most(available_rect.width());
261 side.set_rect_width(&mut panel_rect, width);
262 }
263 }
264 }
265 }
266
267 let mut panel_ui = ui.new_child(
268 UiBuilder::new()
269 .id_salt(id)
270 .ui_stack_info(UiStackInfo::new(match side {
271 Side::Left => UiKind::LeftPanel,
272 Side::Right => UiKind::RightPanel,
273 }))
274 .max_rect(panel_rect)
275 .layout(Layout::top_down(Align::Min)),
276 );
277 panel_ui.expand_to_include_rect(panel_rect);
278 panel_ui.set_clip_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
281 let inner_response = frame.show(&mut panel_ui, |ui| {
282 ui.set_min_height(ui.max_rect().height()); ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0));
284 add_contents(ui)
285 });
286
287 let rect = inner_response.response.rect;
288
289 {
290 let mut cursor = ui.cursor();
291 match side {
292 Side::Left => {
293 cursor.min.x = rect.max.x;
294 }
295 Side::Right => {
296 cursor.max.x = rect.min.x;
297 }
298 }
299 ui.set_cursor(cursor);
300 }
301 ui.expand_to_include_rect(rect);
302
303 if resizable {
304 let resize_x = side.opposite().side_x(panel_rect);
308 let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range())
309 .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0));
310 let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
311 resize_hover = resize_response.hovered();
312 is_resizing = resize_response.dragged();
313 }
314
315 if resize_hover || is_resizing {
316 let cursor_icon = if width <= width_range.min {
317 match self.side {
318 Side::Left => CursorIcon::ResizeEast,
319 Side::Right => CursorIcon::ResizeWest,
320 }
321 } else if width < width_range.max {
322 CursorIcon::ResizeHorizontal
323 } else {
324 match self.side {
325 Side::Left => CursorIcon::ResizeWest,
326 Side::Right => CursorIcon::ResizeEast,
327 }
328 };
329 ui.ctx().set_cursor_icon(cursor_icon);
330 }
331
332 PanelState { rect }.store(ui.ctx(), id);
333
334 {
335 let stroke = if is_resizing {
336 ui.style().visuals.widgets.active.fg_stroke } else if resize_hover {
338 ui.style().visuals.widgets.hovered.fg_stroke } else if show_separator_line {
340 ui.style().visuals.widgets.noninteractive.bg_stroke } else {
343 Stroke::NONE
344 };
345 let resize_x = side.opposite().side_x(rect);
347
348 let resize_x = ui.painter().round_to_pixel_center(resize_x);
350
351 let resize_x = resize_x - if side == Side::Left { 1.0 } else { 0.0 };
354 ui.painter().vline(resize_x, panel_rect.y_range(), stroke);
355 }
356
357 inner_response
358 }
359
360 pub fn show<R>(
362 self,
363 ctx: &Context,
364 add_contents: impl FnOnce(&mut Ui) -> R,
365 ) -> InnerResponse<R> {
366 self.show_dyn(ctx, Box::new(add_contents))
367 }
368
369 fn show_dyn<'c, R>(
371 self,
372 ctx: &Context,
373 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
374 ) -> InnerResponse<R> {
375 let side = self.side;
376 let available_rect = ctx.available_rect();
377 let mut panel_ui = Ui::new(
378 ctx.clone(),
379 self.id,
380 UiBuilder::new()
381 .layer_id(LayerId::background())
382 .max_rect(available_rect),
383 );
384 panel_ui.set_clip_rect(ctx.screen_rect());
385
386 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
387 let rect = inner_response.response.rect;
388
389 match side {
390 Side::Left => ctx.pass_state_mut(|state| {
391 state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
392 }),
393 Side::Right => ctx.pass_state_mut(|state| {
394 state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
395 }),
396 }
397 inner_response
398 }
399
400 pub fn show_animated<R>(
403 self,
404 ctx: &Context,
405 is_expanded: bool,
406 add_contents: impl FnOnce(&mut Ui) -> R,
407 ) -> Option<InnerResponse<R>> {
408 let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
409
410 if 0.0 == how_expanded {
411 None
412 } else if how_expanded < 1.0 {
413 let expanded_width = PanelState::load(ctx, self.id)
417 .map_or(self.default_width, |state| state.rect.width());
418 let fake_width = how_expanded * expanded_width;
419 Self {
420 id: self.id.with("animating_panel"),
421 ..self
422 }
423 .resizable(false)
424 .exact_width(fake_width)
425 .show(ctx, |_ui| {});
426 None
427 } else {
428 Some(self.show(ctx, add_contents))
430 }
431 }
432
433 pub fn show_animated_inside<R>(
436 self,
437 ui: &mut Ui,
438 is_expanded: bool,
439 add_contents: impl FnOnce(&mut Ui) -> R,
440 ) -> Option<InnerResponse<R>> {
441 let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
442
443 if 0.0 == how_expanded {
444 None
445 } else if how_expanded < 1.0 {
446 let expanded_width = PanelState::load(ui.ctx(), self.id)
450 .map_or(self.default_width, |state| state.rect.width());
451 let fake_width = how_expanded * expanded_width;
452 Self {
453 id: self.id.with("animating_panel"),
454 ..self
455 }
456 .resizable(false)
457 .exact_width(fake_width)
458 .show_inside(ui, |_ui| {});
459 None
460 } else {
461 Some(self.show_inside(ui, add_contents))
463 }
464 }
465
466 pub fn show_animated_between<R>(
468 ctx: &Context,
469 is_expanded: bool,
470 collapsed_panel: Self,
471 expanded_panel: Self,
472 add_contents: impl FnOnce(&mut Ui, f32) -> R,
473 ) -> Option<InnerResponse<R>> {
474 let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
475
476 if 0.0 == how_expanded {
477 Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
478 } else if how_expanded < 1.0 {
479 let collapsed_width = PanelState::load(ctx, collapsed_panel.id)
481 .map_or(collapsed_panel.default_width, |state| state.rect.width());
482 let expanded_width = PanelState::load(ctx, expanded_panel.id)
483 .map_or(expanded_panel.default_width, |state| state.rect.width());
484 let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
485 Self {
486 id: expanded_panel.id.with("animating_panel"),
487 ..expanded_panel
488 }
489 .resizable(false)
490 .exact_width(fake_width)
491 .show(ctx, |ui| add_contents(ui, how_expanded));
492 None
493 } else {
494 Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
495 }
496 }
497
498 pub fn show_animated_between_inside<R>(
500 ui: &mut Ui,
501 is_expanded: bool,
502 collapsed_panel: Self,
503 expanded_panel: Self,
504 add_contents: impl FnOnce(&mut Ui, f32) -> R,
505 ) -> InnerResponse<R> {
506 let how_expanded =
507 animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
508
509 if 0.0 == how_expanded {
510 collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
511 } else if how_expanded < 1.0 {
512 let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id)
514 .map_or(collapsed_panel.default_width, |state| state.rect.width());
515 let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id)
516 .map_or(expanded_panel.default_width, |state| state.rect.width());
517 let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
518 Self {
519 id: expanded_panel.id.with("animating_panel"),
520 ..expanded_panel
521 }
522 .resizable(false)
523 .exact_width(fake_width)
524 .show_inside(ui, |ui| add_contents(ui, how_expanded))
525 } else {
526 expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
527 }
528 }
529}
530
531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
535pub enum TopBottomSide {
536 Top,
537 Bottom,
538}
539
540impl TopBottomSide {
541 fn opposite(self) -> Self {
542 match self {
543 Self::Top => Self::Bottom,
544 Self::Bottom => Self::Top,
545 }
546 }
547
548 fn set_rect_height(self, rect: &mut Rect, height: f32) {
549 match self {
550 Self::Top => rect.max.y = rect.min.y + height,
551 Self::Bottom => rect.min.y = rect.max.y - height,
552 }
553 }
554
555 fn side_y(self, rect: Rect) -> f32 {
556 match self {
557 Self::Top => rect.top(),
558 Self::Bottom => rect.bottom(),
559 }
560 }
561}
562
563#[must_use = "You should call .show()"]
582pub struct TopBottomPanel {
583 side: TopBottomSide,
584 id: Id,
585 frame: Option<Frame>,
586 resizable: bool,
587 show_separator_line: bool,
588 default_height: Option<f32>,
589 height_range: Rangef,
590}
591
592impl TopBottomPanel {
593 pub fn top(id: impl Into<Id>) -> Self {
595 Self::new(TopBottomSide::Top, id)
596 }
597
598 pub fn bottom(id: impl Into<Id>) -> Self {
600 Self::new(TopBottomSide::Bottom, id)
601 }
602
603 pub fn new(side: TopBottomSide, id: impl Into<Id>) -> Self {
605 Self {
606 side,
607 id: id.into(),
608 frame: None,
609 resizable: false,
610 show_separator_line: true,
611 default_height: None,
612 height_range: Rangef::new(20.0, f32::INFINITY),
613 }
614 }
615
616 #[inline]
628 pub fn resizable(mut self, resizable: bool) -> Self {
629 self.resizable = resizable;
630 self
631 }
632
633 #[inline]
637 pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
638 self.show_separator_line = show_separator_line;
639 self
640 }
641
642 #[inline]
645 pub fn default_height(mut self, default_height: f32) -> Self {
646 self.default_height = Some(default_height);
647 self.height_range = Rangef::new(
648 self.height_range.min.at_most(default_height),
649 self.height_range.max.at_least(default_height),
650 );
651 self
652 }
653
654 #[inline]
656 pub fn min_height(mut self, min_height: f32) -> Self {
657 self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
658 self
659 }
660
661 #[inline]
663 pub fn max_height(mut self, max_height: f32) -> Self {
664 self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
665 self
666 }
667
668 #[inline]
670 pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
671 let height_range = height_range.into();
672 self.default_height = self
673 .default_height
674 .map(|default_height| clamp_to_range(default_height, height_range));
675 self.height_range = height_range;
676 self
677 }
678
679 #[inline]
681 pub fn exact_height(mut self, height: f32) -> Self {
682 self.default_height = Some(height);
683 self.height_range = Rangef::point(height);
684 self
685 }
686
687 #[inline]
689 pub fn frame(mut self, frame: Frame) -> Self {
690 self.frame = Some(frame);
691 self
692 }
693}
694
695impl TopBottomPanel {
696 pub fn show_inside<R>(
698 self,
699 ui: &mut Ui,
700 add_contents: impl FnOnce(&mut Ui) -> R,
701 ) -> InnerResponse<R> {
702 self.show_inside_dyn(ui, Box::new(add_contents))
703 }
704
705 fn show_inside_dyn<'c, R>(
707 self,
708 ui: &mut Ui,
709 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
710 ) -> InnerResponse<R> {
711 let Self {
712 side,
713 id,
714 frame,
715 resizable,
716 show_separator_line,
717 default_height,
718 height_range,
719 } = self;
720
721 let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
722
723 let available_rect = ui.available_rect_before_wrap();
724 let mut panel_rect = available_rect;
725
726 let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
727 state.rect.height()
728 } else {
729 default_height
730 .unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y)
731 };
732 {
733 height = clamp_to_range(height, height_range).at_most(available_rect.height());
734 side.set_rect_height(&mut panel_rect, height);
735 ui.ctx()
736 .check_for_id_clash(id, panel_rect, "TopBottomPanel");
737 }
738
739 let resize_id = id.with("__resize");
740 let mut resize_hover = false;
741 let mut is_resizing = false;
742 if resizable {
743 if let Some(resize_response) = ui.ctx().read_response(resize_id) {
745 resize_hover = resize_response.hovered();
746 is_resizing = resize_response.dragged();
747
748 if is_resizing {
749 if let Some(pointer) = resize_response.interact_pointer_pos() {
750 height = (pointer.y - side.side_y(panel_rect)).abs();
751 height =
752 clamp_to_range(height, height_range).at_most(available_rect.height());
753 side.set_rect_height(&mut panel_rect, height);
754 }
755 }
756 }
757 }
758
759 let mut panel_ui = ui.new_child(
760 UiBuilder::new()
761 .id_salt(id)
762 .ui_stack_info(UiStackInfo::new(match side {
763 TopBottomSide::Top => UiKind::TopPanel,
764 TopBottomSide::Bottom => UiKind::BottomPanel,
765 }))
766 .max_rect(panel_rect)
767 .layout(Layout::top_down(Align::Min)),
768 );
769 panel_ui.expand_to_include_rect(panel_rect);
770 panel_ui.set_clip_rect(panel_rect); let inner_response = frame.show(&mut panel_ui, |ui| {
773 ui.set_min_width(ui.max_rect().width()); ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0));
775 add_contents(ui)
776 });
777
778 let rect = inner_response.response.rect;
779
780 {
781 let mut cursor = ui.cursor();
782 match side {
783 TopBottomSide::Top => {
784 cursor.min.y = rect.max.y;
785 }
786 TopBottomSide::Bottom => {
787 cursor.max.y = rect.min.y;
788 }
789 }
790 ui.set_cursor(cursor);
791 }
792 ui.expand_to_include_rect(rect);
793
794 if resizable {
795 let resize_y = side.opposite().side_y(panel_rect);
800 let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y)
801 .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));
802 let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
803 resize_hover = resize_response.hovered();
804 is_resizing = resize_response.dragged();
805 }
806
807 if resize_hover || is_resizing {
808 let cursor_icon = if height <= height_range.min {
809 match self.side {
810 TopBottomSide::Top => CursorIcon::ResizeSouth,
811 TopBottomSide::Bottom => CursorIcon::ResizeNorth,
812 }
813 } else if height < height_range.max {
814 CursorIcon::ResizeVertical
815 } else {
816 match self.side {
817 TopBottomSide::Top => CursorIcon::ResizeNorth,
818 TopBottomSide::Bottom => CursorIcon::ResizeSouth,
819 }
820 };
821 ui.ctx().set_cursor_icon(cursor_icon);
822 }
823
824 PanelState { rect }.store(ui.ctx(), id);
825
826 {
827 let stroke = if is_resizing {
828 ui.style().visuals.widgets.active.fg_stroke } else if resize_hover {
830 ui.style().visuals.widgets.hovered.fg_stroke } else if show_separator_line {
832 ui.style().visuals.widgets.noninteractive.bg_stroke } else {
835 Stroke::NONE
836 };
837 let resize_y = side.opposite().side_y(rect);
839
840 let resize_y = ui.painter().round_to_pixel_center(resize_y);
842
843 let resize_y = resize_y - if side == TopBottomSide::Top { 1.0 } else { 0.0 };
846 ui.painter().hline(panel_rect.x_range(), resize_y, stroke);
847 }
848
849 inner_response
850 }
851
852 pub fn show<R>(
854 self,
855 ctx: &Context,
856 add_contents: impl FnOnce(&mut Ui) -> R,
857 ) -> InnerResponse<R> {
858 self.show_dyn(ctx, Box::new(add_contents))
859 }
860
861 fn show_dyn<'c, R>(
863 self,
864 ctx: &Context,
865 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
866 ) -> InnerResponse<R> {
867 let available_rect = ctx.available_rect();
868 let side = self.side;
869
870 let mut panel_ui = Ui::new(
871 ctx.clone(),
872 self.id,
873 UiBuilder::new()
874 .layer_id(LayerId::background())
875 .max_rect(available_rect),
876 );
877 panel_ui.set_clip_rect(ctx.screen_rect());
878
879 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
880 let rect = inner_response.response.rect;
881
882 match side {
883 TopBottomSide::Top => {
884 ctx.pass_state_mut(|state| {
885 state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
886 });
887 }
888 TopBottomSide::Bottom => {
889 ctx.pass_state_mut(|state| {
890 state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
891 });
892 }
893 }
894
895 inner_response
896 }
897
898 pub fn show_animated<R>(
901 self,
902 ctx: &Context,
903 is_expanded: bool,
904 add_contents: impl FnOnce(&mut Ui) -> R,
905 ) -> Option<InnerResponse<R>> {
906 let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
907
908 if 0.0 == how_expanded {
909 None
910 } else if how_expanded < 1.0 {
911 let expanded_height = PanelState::load(ctx, self.id)
915 .map(|state| state.rect.height())
916 .or(self.default_height)
917 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
918 let fake_height = how_expanded * expanded_height;
919 Self {
920 id: self.id.with("animating_panel"),
921 ..self
922 }
923 .resizable(false)
924 .exact_height(fake_height)
925 .show(ctx, |_ui| {});
926 None
927 } else {
928 Some(self.show(ctx, add_contents))
930 }
931 }
932
933 pub fn show_animated_inside<R>(
936 self,
937 ui: &mut Ui,
938 is_expanded: bool,
939 add_contents: impl FnOnce(&mut Ui) -> R,
940 ) -> Option<InnerResponse<R>> {
941 let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
942
943 if 0.0 == how_expanded {
944 None
945 } else if how_expanded < 1.0 {
946 let expanded_height = PanelState::load(ui.ctx(), self.id)
950 .map(|state| state.rect.height())
951 .or(self.default_height)
952 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
953 let fake_height = how_expanded * expanded_height;
954 Self {
955 id: self.id.with("animating_panel"),
956 ..self
957 }
958 .resizable(false)
959 .exact_height(fake_height)
960 .show_inside(ui, |_ui| {});
961 None
962 } else {
963 Some(self.show_inside(ui, add_contents))
965 }
966 }
967
968 pub fn show_animated_between<R>(
970 ctx: &Context,
971 is_expanded: bool,
972 collapsed_panel: Self,
973 expanded_panel: Self,
974 add_contents: impl FnOnce(&mut Ui, f32) -> R,
975 ) -> Option<InnerResponse<R>> {
976 let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
977
978 if 0.0 == how_expanded {
979 Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
980 } else if how_expanded < 1.0 {
981 let collapsed_height = PanelState::load(ctx, collapsed_panel.id)
983 .map(|state| state.rect.height())
984 .or(collapsed_panel.default_height)
985 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
986
987 let expanded_height = PanelState::load(ctx, expanded_panel.id)
988 .map(|state| state.rect.height())
989 .or(expanded_panel.default_height)
990 .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
991
992 let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
993 Self {
994 id: expanded_panel.id.with("animating_panel"),
995 ..expanded_panel
996 }
997 .resizable(false)
998 .exact_height(fake_height)
999 .show(ctx, |ui| add_contents(ui, how_expanded));
1000 None
1001 } else {
1002 Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
1003 }
1004 }
1005
1006 pub fn show_animated_between_inside<R>(
1008 ui: &mut Ui,
1009 is_expanded: bool,
1010 collapsed_panel: Self,
1011 expanded_panel: Self,
1012 add_contents: impl FnOnce(&mut Ui, f32) -> R,
1013 ) -> InnerResponse<R> {
1014 let how_expanded =
1015 animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
1016
1017 if 0.0 == how_expanded {
1018 collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1019 } else if how_expanded < 1.0 {
1020 let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id)
1022 .map(|state| state.rect.height())
1023 .or(collapsed_panel.default_height)
1024 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1025
1026 let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id)
1027 .map(|state| state.rect.height())
1028 .or(expanded_panel.default_height)
1029 .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1030
1031 let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
1032 Self {
1033 id: expanded_panel.id.with("animating_panel"),
1034 ..expanded_panel
1035 }
1036 .resizable(false)
1037 .exact_height(fake_height)
1038 .show_inside(ui, |ui| add_contents(ui, how_expanded))
1039 } else {
1040 expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1041 }
1042 }
1043}
1044
1045#[must_use = "You should call .show()"]
1070#[derive(Default)]
1071pub struct CentralPanel {
1072 frame: Option<Frame>,
1073}
1074
1075impl CentralPanel {
1076 #[inline]
1078 pub fn frame(mut self, frame: Frame) -> Self {
1079 self.frame = Some(frame);
1080 self
1081 }
1082}
1083
1084impl CentralPanel {
1085 pub fn show_inside<R>(
1087 self,
1088 ui: &mut Ui,
1089 add_contents: impl FnOnce(&mut Ui) -> R,
1090 ) -> InnerResponse<R> {
1091 self.show_inside_dyn(ui, Box::new(add_contents))
1092 }
1093
1094 fn show_inside_dyn<'c, R>(
1096 self,
1097 ui: &mut Ui,
1098 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1099 ) -> InnerResponse<R> {
1100 let Self { frame } = self;
1101
1102 let panel_rect = ui.available_rect_before_wrap();
1103 let mut panel_ui = ui.new_child(
1104 UiBuilder::new()
1105 .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
1106 .max_rect(panel_rect)
1107 .layout(Layout::top_down(Align::Min)),
1108 );
1109 panel_ui.set_clip_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
1112 frame.show(&mut panel_ui, |ui| {
1113 ui.expand_to_include_rect(ui.max_rect()); add_contents(ui)
1115 })
1116 }
1117
1118 pub fn show<R>(
1120 self,
1121 ctx: &Context,
1122 add_contents: impl FnOnce(&mut Ui) -> R,
1123 ) -> InnerResponse<R> {
1124 self.show_dyn(ctx, Box::new(add_contents))
1125 }
1126
1127 fn show_dyn<'c, R>(
1129 self,
1130 ctx: &Context,
1131 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1132 ) -> InnerResponse<R> {
1133 let available_rect = ctx.available_rect();
1134 let id = Id::new((ctx.viewport_id(), "central_panel"));
1135
1136 let mut panel_ui = Ui::new(
1137 ctx.clone(),
1138 id,
1139 UiBuilder::new()
1140 .layer_id(LayerId::background())
1141 .max_rect(available_rect),
1142 );
1143 panel_ui.set_clip_rect(ctx.screen_rect());
1144
1145 let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
1146
1147 ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
1149
1150 inner_response
1151 }
1152}
1153
1154fn clamp_to_range(x: f32, range: Rangef) -> f32 {
1155 let range = range.as_positive();
1156 x.clamp(range.min, range.max)
1157}