1use std::sync::Arc;
4
5use crate::collapsing_header::CollapsingState;
6use crate::{
7 Align, Align2, Context, CursorIcon, Id, InnerResponse, LayerId, NumExt, Order, Response, Sense,
8 TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType,
9};
10use epaint::{emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Shape, Stroke, Vec2};
11
12use super::scroll_area::ScrollBarVisibility;
13use super::{area, resize, Area, Frame, Resize, ScrollArea};
14
15#[must_use = "You should call .show()"]
37pub struct Window<'open> {
38 title: WidgetText,
39 open: Option<&'open mut bool>,
40 area: Area,
41 frame: Option<Frame>,
42 resize: Resize,
43 scroll: ScrollArea,
44 collapsible: bool,
45 default_open: bool,
46 with_title_bar: bool,
47 fade_out: bool,
48}
49
50impl<'open> Window<'open> {
51 pub fn new(title: impl Into<WidgetText>) -> Self {
55 let title = title.into().fallback_text_style(TextStyle::Heading);
56 let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
57 Self {
58 title,
59 open: None,
60 area,
61 frame: None,
62 resize: Resize::default()
63 .with_stroke(false)
64 .min_size([96.0, 32.0])
65 .default_size([340.0, 420.0]), scroll: ScrollArea::neither().auto_shrink(false),
67 collapsible: true,
68 default_open: true,
69 with_title_bar: true,
70 fade_out: true,
71 }
72 }
73
74 #[inline]
76 pub fn id(mut self, id: Id) -> Self {
77 self.area = self.area.id(id);
78 self
79 }
80
81 #[inline]
87 pub fn open(mut self, open: &'open mut bool) -> Self {
88 self.open = Some(open);
89 self
90 }
91
92 #[inline]
94 pub fn enabled(mut self, enabled: bool) -> Self {
95 self.area = self.area.enabled(enabled);
96 self
97 }
98
99 #[inline]
105 pub fn interactable(mut self, interactable: bool) -> Self {
106 self.area = self.area.interactable(interactable);
107 self
108 }
109
110 #[inline]
112 pub fn movable(mut self, movable: bool) -> Self {
113 self.area = self.area.movable(movable);
114 self
115 }
116
117 #[inline]
119 pub fn order(mut self, order: Order) -> Self {
120 self.area = self.area.order(order);
121 self
122 }
123
124 #[inline]
128 pub fn fade_in(mut self, fade_in: bool) -> Self {
129 self.area = self.area.fade_in(fade_in);
130 self
131 }
132
133 #[inline]
139 pub fn fade_out(mut self, fade_out: bool) -> Self {
140 self.fade_out = fade_out;
141 self
142 }
143
144 #[inline]
147 pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
148 mutate(&mut self);
149 self
150 }
151
152 #[inline]
155 pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
156 self.resize = mutate(self.resize);
157 self
158 }
159
160 #[inline]
162 pub fn frame(mut self, frame: Frame) -> Self {
163 self.frame = Some(frame);
164 self
165 }
166
167 #[inline]
169 pub fn min_width(mut self, min_width: f32) -> Self {
170 self.resize = self.resize.min_width(min_width);
171 self
172 }
173
174 #[inline]
176 pub fn min_height(mut self, min_height: f32) -> Self {
177 self.resize = self.resize.min_height(min_height);
178 self
179 }
180
181 #[inline]
183 pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
184 self.resize = self.resize.min_size(min_size);
185 self
186 }
187
188 #[inline]
190 pub fn max_width(mut self, max_width: f32) -> Self {
191 self.resize = self.resize.max_width(max_width);
192 self
193 }
194
195 #[inline]
197 pub fn max_height(mut self, max_height: f32) -> Self {
198 self.resize = self.resize.max_height(max_height);
199 self
200 }
201
202 #[inline]
204 pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
205 self.resize = self.resize.max_size(max_size);
206 self
207 }
208
209 #[inline]
212 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
213 self.area = self.area.current_pos(current_pos);
214 self
215 }
216
217 #[inline]
219 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
220 self.area = self.area.default_pos(default_pos);
221 self
222 }
223
224 #[inline]
226 pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
227 self.area = self.area.fixed_pos(pos);
228 self
229 }
230
231 #[inline]
237 pub fn constrain(mut self, constrain: bool) -> Self {
238 self.area = self.area.constrain(constrain);
239 self
240 }
241
242 #[inline]
246 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
247 self.area = self.area.constrain_to(constrain_rect);
248 self
249 }
250
251 #[inline]
259 pub fn pivot(mut self, pivot: Align2) -> Self {
260 self.area = self.area.pivot(pivot);
261 self
262 }
263
264 #[inline]
276 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
277 self.area = self.area.anchor(align, offset);
278 self
279 }
280
281 #[inline]
283 pub fn default_open(mut self, default_open: bool) -> Self {
284 self.default_open = default_open;
285 self
286 }
287
288 #[inline]
290 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
291 let default_size: Vec2 = default_size.into();
292 self.resize = self.resize.default_size(default_size);
293 self.area = self.area.default_size(default_size);
294 self
295 }
296
297 #[inline]
299 pub fn default_width(mut self, default_width: f32) -> Self {
300 self.resize = self.resize.default_width(default_width);
301 self.area = self.area.default_width(default_width);
302 self
303 }
304
305 #[inline]
307 pub fn default_height(mut self, default_height: f32) -> Self {
308 self.resize = self.resize.default_height(default_height);
309 self.area = self.area.default_height(default_height);
310 self
311 }
312
313 #[inline]
315 pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
316 self.resize = self.resize.fixed_size(size);
317 self
318 }
319
320 pub fn default_rect(self, rect: Rect) -> Self {
322 self.default_pos(rect.min).default_size(rect.size())
323 }
324
325 pub fn fixed_rect(self, rect: Rect) -> Self {
327 self.fixed_pos(rect.min).fixed_size(rect.size())
328 }
329
330 #[inline]
340 pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
341 let resizable = resizable.into();
342 self.resize = self.resize.resizable(resizable);
343 self
344 }
345
346 #[inline]
348 pub fn collapsible(mut self, collapsible: bool) -> Self {
349 self.collapsible = collapsible;
350 self
351 }
352
353 #[inline]
356 pub fn title_bar(mut self, title_bar: bool) -> Self {
357 self.with_title_bar = title_bar;
358 self
359 }
360
361 #[inline]
365 pub fn auto_sized(mut self) -> Self {
366 self.resize = self.resize.auto_sized();
367 self.scroll = ScrollArea::neither();
368 self
369 }
370
371 #[inline]
375 pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
376 self.scroll = self.scroll.scroll(scroll);
377 self
378 }
379
380 #[deprecated = "Renamed to `scroll`"]
382 #[inline]
383 pub fn scroll2(mut self, scroll: impl Into<Vec2b>) -> Self {
384 self.scroll = self.scroll.scroll(scroll);
385 self
386 }
387
388 #[inline]
390 pub fn hscroll(mut self, hscroll: bool) -> Self {
391 self.scroll = self.scroll.hscroll(hscroll);
392 self
393 }
394
395 #[inline]
397 pub fn vscroll(mut self, vscroll: bool) -> Self {
398 self.scroll = self.scroll.vscroll(vscroll);
399 self
400 }
401
402 #[inline]
406 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
407 self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
408 self
409 }
410
411 #[inline]
413 pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
414 self.scroll = self.scroll.scroll_bar_visibility(visibility);
415 self
416 }
417}
418
419impl<'open> Window<'open> {
420 #[inline]
423 pub fn show<R>(
424 self,
425 ctx: &Context,
426 add_contents: impl FnOnce(&mut Ui) -> R,
427 ) -> Option<InnerResponse<Option<R>>> {
428 self.show_dyn(ctx, Box::new(add_contents))
429 }
430
431 fn show_dyn<'c, R>(
432 self,
433 ctx: &Context,
434 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
435 ) -> Option<InnerResponse<Option<R>>> {
436 let Window {
437 title,
438 open,
439 area,
440 frame,
441 resize,
442 scroll,
443 collapsible,
444 default_open,
445 with_title_bar,
446 fade_out,
447 } = self;
448
449 let header_color =
450 frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
451 let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
452 let window_margin = window_frame.inner_margin;
454
455 let is_explicitly_closed = matches!(open, Some(false));
456 let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
457 let opacity = ctx.animate_bool_with_easing(
458 area.id.with("fade-out"),
459 is_open,
460 emath::easing::cubic_out,
461 );
462 if opacity <= 0.0 {
463 return None;
464 }
465
466 let area_id = area.id;
467 let area_layer_id = area.layer();
468 let resize_id = area_id.with("resize");
469 let mut collapsing =
470 CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
471
472 let is_collapsed = with_title_bar && !collapsing.is_open();
473 let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
474
475 let resize = resize.resizable(false); let mut resize = resize.id(resize_id);
477
478 let on_top = Some(area_layer_id) == ctx.top_layer_id();
479 let mut area = area.begin(ctx);
480
481 area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
482
483 let (title_bar_height, title_content_spacing) = if with_title_bar {
485 let style = ctx.style();
486 let spacing = window_margin.top + window_margin.bottom;
487 let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing;
488 window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0);
489 window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0);
490 (height, spacing)
491 } else {
492 (0.0, 0.0)
493 };
494
495 {
496 let constrain_rect = area.constrain_rect();
498 let max_width = constrain_rect.width();
499 let max_height = constrain_rect.height() - title_bar_height;
500 resize.max_size.x = resize.max_size.x.min(max_width);
501 resize.max_size.y = resize.max_size.y.min(max_height);
502 }
503
504 let last_frame_outer_rect = area.state().rect();
506 let resize_interaction = ctx.with_accessibility_parent(area.id(), || {
507 resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect)
508 });
509
510 let margins = window_frame.outer_margin.sum()
511 + window_frame.inner_margin.sum()
512 + vec2(0.0, title_bar_height);
513
514 resize_response(
515 resize_interaction,
516 ctx,
517 margins,
518 area_layer_id,
519 &mut area,
520 resize_id,
521 );
522
523 let mut area_content_ui = area.content_ui(ctx);
524 if is_open {
525 } else if fade_out {
528 area_content_ui.multiply_opacity(opacity);
529 }
530
531 let content_inner = {
532 ctx.with_accessibility_parent(area.id(), || {
533 let frame_stroke = window_frame.stroke;
535 let mut frame = window_frame.begin(&mut area_content_ui);
536
537 let show_close_button = open.is_some();
538
539 let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
540
541 let item_spacing = frame.content_ui.spacing().item_spacing;
543 frame.content_ui.spacing_mut().item_spacing.y = title_content_spacing;
545
546 let title_bar = if with_title_bar {
547 let title_bar = TitleBar::new(
548 &mut frame.content_ui,
549 title,
550 show_close_button,
551 &mut collapsing,
552 collapsible,
553 );
554 resize.min_size.x = resize.min_size.x.at_least(title_bar.rect.width()); Some(title_bar)
556 } else {
557 None
558 };
559
560 frame.content_ui.spacing_mut().item_spacing.y = 0.0;
562
563 let (content_inner, mut content_response) = collapsing
564 .show_body_unindented(&mut frame.content_ui, |ui| {
565 ui.spacing_mut().item_spacing.y = item_spacing.y;
567
568 resize.show(ui, |ui| {
569 if scroll.is_any_scroll_enabled() {
570 scroll.show(ui, add_contents).inner
571 } else {
572 add_contents(ui)
573 }
574 })
575 })
576 .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
577
578 let outer_rect = frame.end(&mut area_content_ui).rect;
579 paint_resize_corner(
580 &area_content_ui,
581 &possible,
582 outer_rect,
583 frame_stroke,
584 window_frame.rounding,
585 );
586
587 if let Some(title_bar) = title_bar {
590 let mut title_rect = Rect::from_min_size(
591 outer_rect.min,
592 Vec2 {
593 x: outer_rect.size().x,
594 y: title_bar_height,
595 },
596 );
597
598 title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect);
599
600 if on_top && area_content_ui.visuals().window_highlight_topmost {
601 let mut round = window_frame.rounding;
602
603 if !is_collapsed {
604 round.se = 0.0;
605 round.sw = 0.0;
606 }
607
608 area_content_ui.painter().set(
609 *where_to_put_header_background,
610 RectShape::filled(title_rect, round, header_color),
611 );
612 };
613
614 if let Some(response) = &mut content_response {
616 response.rect.min.y = outer_rect.min.y + title_bar_height;
617 }
618
619 title_bar.ui(
620 &mut area_content_ui,
621 title_rect,
622 &content_response,
623 open,
624 &mut collapsing,
625 collapsible,
626 );
627 }
628
629 collapsing.store(ctx);
630
631 paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
632
633 content_inner
634 })
635 };
636
637 let full_response = area.end(ctx, area_content_ui);
638
639 let inner_response = InnerResponse {
640 inner: content_inner,
641 response: full_response,
642 };
643 Some(inner_response)
644 }
645}
646
647fn paint_resize_corner(
648 ui: &Ui,
649 possible: &PossibleInteractions,
650 outer_rect: Rect,
651 stroke: impl Into<Stroke>,
652 rounding: impl Into<Rounding>,
653) {
654 let stroke = stroke.into();
655 let rounding = rounding.into();
656 let (corner, radius) = if possible.resize_right && possible.resize_bottom {
657 (Align2::RIGHT_BOTTOM, rounding.se)
658 } else if possible.resize_left && possible.resize_bottom {
659 (Align2::LEFT_BOTTOM, rounding.sw)
660 } else if possible.resize_left && possible.resize_top {
661 (Align2::LEFT_TOP, rounding.nw)
662 } else if possible.resize_right && possible.resize_top {
663 (Align2::RIGHT_TOP, rounding.ne)
664 } else {
665 if possible.resize_right || possible.resize_bottom {
669 (Align2::RIGHT_BOTTOM, rounding.se)
670 } else if possible.resize_left || possible.resize_bottom {
671 (Align2::LEFT_BOTTOM, rounding.sw)
672 } else if possible.resize_left || possible.resize_top {
673 (Align2::LEFT_TOP, rounding.nw)
674 } else if possible.resize_right || possible.resize_top {
675 (Align2::RIGHT_TOP, rounding.ne)
676 } else {
677 return;
678 }
679 };
680
681 let offset =
683 ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
684
685 let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
686 let corner_rect = corner.align_size_within_rect(corner_size, outer_rect);
687 let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
689}
690
691#[derive(Clone, Copy, Debug)]
695struct PossibleInteractions {
696 resize_left: bool,
698 resize_right: bool,
699 resize_top: bool,
700 resize_bottom: bool,
701}
702
703impl PossibleInteractions {
704 fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
705 let movable = area.is_enabled() && area.is_movable();
706 let resizable = resize
707 .is_resizable()
708 .and(area.is_enabled() && !is_collapsed);
709 let pivot = area.get_pivot();
710 Self {
711 resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
712 resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
713 resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
714 resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
715 }
716 }
717
718 pub fn resizable(&self) -> bool {
719 self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
720 }
721}
722
723#[derive(Clone, Copy, Debug)]
725struct ResizeInteraction {
726 start_rect: Rect,
727 left: SideResponse,
728 right: SideResponse,
729 top: SideResponse,
730 bottom: SideResponse,
731}
732
733#[derive(Clone, Copy, Debug, Default)]
735struct SideResponse {
736 hover: bool,
737 drag: bool,
738}
739
740impl SideResponse {
741 pub fn any(&self) -> bool {
742 self.hover || self.drag
743 }
744}
745
746impl std::ops::BitOrAssign for SideResponse {
747 fn bitor_assign(&mut self, rhs: Self) {
748 *self = Self {
749 hover: self.hover || rhs.hover,
750 drag: self.drag || rhs.drag,
751 };
752 }
753}
754
755impl ResizeInteraction {
756 pub fn set_cursor(&self, ctx: &Context) {
757 let left = self.left.any();
758 let right = self.right.any();
759 let top = self.top.any();
760 let bottom = self.bottom.any();
761
762 if (left && top) || (right && bottom) {
764 ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
765 } else if (right && top) || (left && bottom) {
766 ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
767 } else if left || right {
768 ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
769 } else if bottom || top {
770 ctx.set_cursor_icon(CursorIcon::ResizeVertical);
771 }
772 }
773
774 pub fn any_hovered(&self) -> bool {
775 self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
776 }
777
778 pub fn any_dragged(&self) -> bool {
779 self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
780 }
781}
782
783fn resize_response(
784 resize_interaction: ResizeInteraction,
785 ctx: &Context,
786 margins: Vec2,
787 area_layer_id: LayerId,
788 area: &mut area::Prepared,
789 resize_id: Id,
790) {
791 let Some(new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
792 return;
793 };
794 let mut new_rect = ctx.round_rect_to_pixels(new_rect);
795
796 if area.constrain() {
797 new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect());
798 }
799
800 area.state_mut().set_left_top_pos(new_rect.left_top());
802
803 if resize_interaction.any_dragged() {
804 if let Some(mut state) = resize::State::load(ctx, resize_id) {
805 state.requested_size = Some(new_rect.size() - margins);
806 state.store(ctx, resize_id);
807 }
808 }
809
810 ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
811}
812
813fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
814 if !interaction.any_dragged() {
815 return None;
816 }
817
818 let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
819 let mut rect = interaction.start_rect; if interaction.left.drag {
822 rect.min.x = ctx.round_to_pixel(pointer_pos.x);
823 } else if interaction.right.drag {
824 rect.max.x = ctx.round_to_pixel(pointer_pos.x);
825 }
826
827 if interaction.top.drag {
828 rect.min.y = ctx.round_to_pixel(pointer_pos.y);
829 } else if interaction.bottom.drag {
830 rect.max.y = ctx.round_to_pixel(pointer_pos.y);
831 }
832
833 Some(rect)
834}
835
836fn resize_interaction(
837 ctx: &Context,
838 possible: PossibleInteractions,
839 layer_id: LayerId,
840 rect: Rect,
841) -> ResizeInteraction {
842 if !possible.resizable() {
843 return ResizeInteraction {
844 start_rect: rect,
845 left: Default::default(),
846 right: Default::default(),
847 top: Default::default(),
848 bottom: Default::default(),
849 };
850 }
851
852 let is_dragging = |rect, id| {
853 let response = ctx.create_widget(
854 WidgetRect {
855 layer_id,
856 id,
857 rect,
858 interact_rect: rect,
859 sense: Sense::drag(),
860 enabled: true,
861 },
862 true,
863 );
864 SideResponse {
865 hover: response.hovered(),
866 drag: response.dragged(),
867 }
868 };
869
870 let id = Id::new(layer_id).with("edge_drag");
871
872 let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
873 let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
874
875 let corner_rect =
876 |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
877
878 let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
880
881 if possible.resize_right {
885 let response = is_dragging(
886 Rect::from_min_max(rect.right_top(), rect.right_bottom()).expand(side_grab_radius),
887 id.with("right"),
888 );
889 right |= response;
890 }
891 if possible.resize_left {
892 let response = is_dragging(
893 Rect::from_min_max(rect.left_top(), rect.left_bottom()).expand(side_grab_radius),
894 id.with("left"),
895 );
896 left |= response;
897 }
898 if possible.resize_bottom {
899 let response = is_dragging(
900 Rect::from_min_max(rect.left_bottom(), rect.right_bottom()).expand(side_grab_radius),
901 id.with("bottom"),
902 );
903 bottom |= response;
904 }
905 if possible.resize_top {
906 let response = is_dragging(
907 Rect::from_min_max(rect.left_top(), rect.right_top()).expand(side_grab_radius),
908 id.with("top"),
909 );
910 top |= response;
911 }
912
913 if possible.resize_right && possible.resize_bottom {
917 let response = is_dragging(corner_rect(rect.right_bottom()), id.with("right_bottom"));
918 right |= response;
919 bottom |= response;
920 }
921
922 if possible.resize_right && possible.resize_top {
923 let response = is_dragging(corner_rect(rect.right_top()), id.with("right_top"));
924 right |= response;
925 top |= response;
926 }
927
928 if possible.resize_left && possible.resize_bottom {
929 let response = is_dragging(corner_rect(rect.left_bottom()), id.with("left_bottom"));
930 left |= response;
931 bottom |= response;
932 }
933
934 if possible.resize_left && possible.resize_top {
935 let response = is_dragging(corner_rect(rect.left_top()), id.with("left_top"));
936 left |= response;
937 top |= response;
938 }
939
940 let interaction = ResizeInteraction {
941 start_rect: rect,
942 left,
943 right,
944 top,
945 bottom,
946 };
947 interaction.set_cursor(ctx);
948 interaction
949}
950
951fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
953 use epaint::tessellator::path::add_circle_quadrant;
954
955 let visuals = if interaction.any_dragged() {
956 ui.style().visuals.widgets.active
957 } else if interaction.any_hovered() {
958 ui.style().visuals.widgets.hovered
959 } else {
960 return;
961 };
962
963 let [left, right, top, bottom]: [bool; 4];
964
965 if interaction.any_dragged() {
966 left = interaction.left.drag;
967 right = interaction.right.drag;
968 top = interaction.top.drag;
969 bottom = interaction.bottom.drag;
970 } else {
971 left = interaction.left.hover;
972 right = interaction.right.hover;
973 top = interaction.top.hover;
974 bottom = interaction.bottom.hover;
975 }
976
977 let rounding = ui.visuals().window_rounding;
978 let Rect { min, max } = rect;
979
980 let mut points = Vec::new();
981
982 if right && !bottom && !top {
983 points.push(pos2(max.x, min.y + rounding.ne));
984 points.push(pos2(max.x, max.y - rounding.se));
985 }
986 if right && bottom {
987 points.push(pos2(max.x, min.y + rounding.ne));
988 points.push(pos2(max.x, max.y - rounding.se));
989 add_circle_quadrant(
990 &mut points,
991 pos2(max.x - rounding.se, max.y - rounding.se),
992 rounding.se,
993 0.0,
994 );
995 }
996 if bottom {
997 points.push(pos2(max.x - rounding.se, max.y));
998 points.push(pos2(min.x + rounding.sw, max.y));
999 }
1000 if left && bottom {
1001 add_circle_quadrant(
1002 &mut points,
1003 pos2(min.x + rounding.sw, max.y - rounding.sw),
1004 rounding.sw,
1005 1.0,
1006 );
1007 }
1008 if left {
1009 points.push(pos2(min.x, max.y - rounding.sw));
1010 points.push(pos2(min.x, min.y + rounding.nw));
1011 }
1012 if left && top {
1013 add_circle_quadrant(
1014 &mut points,
1015 pos2(min.x + rounding.nw, min.y + rounding.nw),
1016 rounding.nw,
1017 2.0,
1018 );
1019 }
1020 if top {
1021 points.push(pos2(min.x + rounding.nw, min.y));
1022 points.push(pos2(max.x - rounding.ne, min.y));
1023 }
1024 if right && top {
1025 add_circle_quadrant(
1026 &mut points,
1027 pos2(max.x - rounding.ne, min.y + rounding.ne),
1028 rounding.ne,
1029 3.0,
1030 );
1031 points.push(pos2(max.x, min.y + rounding.ne));
1032 points.push(pos2(max.x, max.y - rounding.se));
1033 }
1034 ui.painter().add(Shape::line(points, visuals.bg_stroke));
1035}
1036
1037struct TitleBar {
1040 id: Id,
1042
1043 title_galley: Arc<Galley>,
1045
1046 min_rect: Rect,
1050
1051 rect: Rect,
1054}
1055
1056impl TitleBar {
1057 fn new(
1058 ui: &mut Ui,
1059 title: WidgetText,
1060 show_close_button: bool,
1061 collapsing: &mut CollapsingState,
1062 collapsible: bool,
1063 ) -> Self {
1064 let inner_response = ui.horizontal(|ui| {
1065 let height = ui
1066 .fonts(|fonts| title.font_height(fonts, ui.style()))
1067 .max(ui.spacing().interact_size.y);
1068 ui.set_min_height(height);
1069
1070 let item_spacing = ui.spacing().item_spacing;
1071 let button_size = Vec2::splat(ui.spacing().icon_width);
1072
1073 let pad = (height - button_size.y) / 2.0; if collapsible {
1076 ui.add_space(pad);
1077 collapsing.show_default_button_with_size(ui, button_size);
1078 }
1079
1080 let title_galley = title.into_galley(
1081 ui,
1082 Some(crate::TextWrapMode::Extend),
1083 f32::INFINITY,
1084 TextStyle::Heading,
1085 );
1086
1087 let minimum_width = if collapsible || show_close_button {
1088 2.0 * (pad + button_size.x + item_spacing.x) + title_galley.size().x
1090 } else {
1091 pad + title_galley.size().x + pad
1092 };
1093 let min_rect = Rect::from_min_size(ui.min_rect().min, vec2(minimum_width, height));
1094 let id = ui.advance_cursor_after_rect(min_rect);
1095
1096 Self {
1097 id,
1098 title_galley,
1099 min_rect,
1100 rect: Rect::NAN, }
1102 });
1103
1104 let title_bar = inner_response.inner;
1105 let rect = inner_response.response.rect;
1106
1107 Self { rect, ..title_bar }
1108 }
1109
1110 fn ui(
1125 mut self,
1126 ui: &mut Ui,
1127 outer_rect: Rect,
1128 content_response: &Option<Response>,
1129 open: Option<&mut bool>,
1130 collapsing: &mut CollapsingState,
1131 collapsible: bool,
1132 ) {
1133 if let Some(content_response) = &content_response {
1134 self.rect.max.x = self.rect.max.x.max(content_response.rect.max.x);
1136 }
1137
1138 if let Some(open) = open {
1139 if self.close_button_ui(ui).clicked() {
1141 *open = false;
1142 }
1143 }
1144
1145 let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
1146 let text_pos =
1147 emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top();
1148 let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
1149 ui.painter().galley(
1150 text_pos,
1151 self.title_galley.clone(),
1152 ui.visuals().text_color(),
1153 );
1154
1155 if let Some(content_response) = &content_response {
1156 let y = content_response.rect.top();
1158 let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
1160 let x_range = outer_rect.x_range().shrink(0.1);
1164 ui.painter().hline(x_range, y, stroke);
1165 }
1166
1167 let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0));
1169
1170 if ui
1171 .interact(double_click_rect, self.id, Sense::click())
1172 .double_clicked()
1173 && collapsible
1174 {
1175 collapsing.toggle(ui);
1176 }
1177 }
1178
1179 fn close_button_ui(&self, ui: &mut Ui) -> Response {
1185 let button_size = Vec2::splat(ui.spacing().icon_width);
1186 let pad = (self.rect.height() - button_size.y) / 2.0; let button_rect = Rect::from_min_size(
1188 pos2(
1189 self.rect.right() - pad - button_size.x,
1190 self.rect.center().y - 0.5 * button_size.y,
1191 ),
1192 button_size,
1193 );
1194
1195 close_button(ui, button_rect)
1196 }
1197}
1198
1199fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1210 let close_id = ui.auto_id_with("window_close_button");
1211 let response = ui.interact(rect, close_id, Sense::click());
1212 response
1213 .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1214
1215 ui.expand_to_include_rect(response.rect);
1216
1217 let visuals = ui.style().interact(&response);
1218 let rect = rect.shrink(2.0).expand(visuals.expansion);
1219 let stroke = visuals.fg_stroke;
1220 ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1222 ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1224 response
1225}