1use std::sync::Arc;
4
5use emath::GuiRounding as _;
6use epaint::{CornerRadiusF32, RectShape};
7
8use crate::collapsing_header::CollapsingState;
9use crate::*;
10
11use super::scroll_area::{ScrollBarVisibility, ScrollSource};
12use super::{Area, Frame, Resize, ScrollArea, area, resize};
13
14#[must_use = "You should call .show()"]
36pub struct Window<'open> {
37 title: WidgetText,
38 open: Option<&'open mut bool>,
39 area: Area,
40 frame: Option<Frame>,
41 resize: Resize,
42 scroll: ScrollArea,
43 collapsible: bool,
44 default_open: bool,
45 with_title_bar: bool,
46 fade_out: bool,
47}
48
49impl<'open> Window<'open> {
50 pub fn new(title: impl Into<WidgetText>) -> Self {
54 let title = title.into().fallback_text_style(TextStyle::Heading);
55 let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
56 Self {
57 title,
58 open: None,
59 area,
60 frame: None,
61 resize: Resize::default()
62 .with_stroke(false)
63 .min_size([96.0, 32.0])
64 .default_size([340.0, 420.0]), scroll: ScrollArea::neither().auto_shrink(false),
66 collapsible: true,
67 default_open: true,
68 with_title_bar: true,
69 fade_out: true,
70 }
71 }
72
73 #[inline]
75 pub fn id(mut self, id: Id) -> Self {
76 self.area = self.area.id(id);
77 self
78 }
79
80 #[inline]
86 pub fn open(mut self, open: &'open mut bool) -> Self {
87 self.open = Some(open);
88 self
89 }
90
91 #[inline]
93 pub fn enabled(mut self, enabled: bool) -> Self {
94 self.area = self.area.enabled(enabled);
95 self
96 }
97
98 #[inline]
104 pub fn interactable(mut self, interactable: bool) -> Self {
105 self.area = self.area.interactable(interactable);
106 self
107 }
108
109 #[inline]
111 pub fn movable(mut self, movable: bool) -> Self {
112 self.area = self.area.movable(movable);
113 self
114 }
115
116 #[inline]
118 pub fn order(mut self, order: Order) -> Self {
119 self.area = self.area.order(order);
120 self
121 }
122
123 #[inline]
127 pub fn fade_in(mut self, fade_in: bool) -> Self {
128 self.area = self.area.fade_in(fade_in);
129 self
130 }
131
132 #[inline]
138 pub fn fade_out(mut self, fade_out: bool) -> Self {
139 self.fade_out = fade_out;
140 self
141 }
142
143 #[inline]
146 pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
147 mutate(&mut self);
148 self
149 }
150
151 #[inline]
154 pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
155 self.resize = mutate(self.resize);
156 self
157 }
158
159 #[inline]
161 pub fn frame(mut self, frame: Frame) -> Self {
162 self.frame = Some(frame);
163 self
164 }
165
166 #[inline]
168 pub fn min_width(mut self, min_width: f32) -> Self {
169 self.resize = self.resize.min_width(min_width);
170 self
171 }
172
173 #[inline]
175 pub fn min_height(mut self, min_height: f32) -> Self {
176 self.resize = self.resize.min_height(min_height);
177 self
178 }
179
180 #[inline]
182 pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
183 self.resize = self.resize.min_size(min_size);
184 self
185 }
186
187 #[inline]
189 pub fn max_width(mut self, max_width: f32) -> Self {
190 self.resize = self.resize.max_width(max_width);
191 self
192 }
193
194 #[inline]
196 pub fn max_height(mut self, max_height: f32) -> Self {
197 self.resize = self.resize.max_height(max_height);
198 self
199 }
200
201 #[inline]
203 pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
204 self.resize = self.resize.max_size(max_size);
205 self
206 }
207
208 #[inline]
211 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
212 self.area = self.area.current_pos(current_pos);
213 self
214 }
215
216 #[inline]
218 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
219 self.area = self.area.default_pos(default_pos);
220 self
221 }
222
223 #[inline]
225 pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
226 self.area = self.area.fixed_pos(pos);
227 self
228 }
229
230 #[inline]
236 pub fn constrain(mut self, constrain: bool) -> Self {
237 self.area = self.area.constrain(constrain);
238 self
239 }
240
241 #[inline]
245 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
246 self.area = self.area.constrain_to(constrain_rect);
247 self
248 }
249
250 #[inline]
258 pub fn pivot(mut self, pivot: Align2) -> Self {
259 self.area = self.area.pivot(pivot);
260 self
261 }
262
263 #[inline]
275 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
276 self.area = self.area.anchor(align, offset);
277 self
278 }
279
280 #[inline]
282 pub fn default_open(mut self, default_open: bool) -> Self {
283 self.default_open = default_open;
284 self
285 }
286
287 #[inline]
289 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
290 let default_size: Vec2 = default_size.into();
291 self.resize = self.resize.default_size(default_size);
292 self.area = self.area.default_size(default_size);
293 self
294 }
295
296 #[inline]
298 pub fn default_width(mut self, default_width: f32) -> Self {
299 self.resize = self.resize.default_width(default_width);
300 self.area = self.area.default_width(default_width);
301 self
302 }
303
304 #[inline]
306 pub fn default_height(mut self, default_height: f32) -> Self {
307 self.resize = self.resize.default_height(default_height);
308 self.area = self.area.default_height(default_height);
309 self
310 }
311
312 #[inline]
314 pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
315 self.resize = self.resize.fixed_size(size);
316 self
317 }
318
319 pub fn default_rect(self, rect: Rect) -> Self {
321 self.default_pos(rect.min).default_size(rect.size())
322 }
323
324 pub fn fixed_rect(self, rect: Rect) -> Self {
326 self.fixed_pos(rect.min).fixed_size(rect.size())
327 }
328
329 #[inline]
339 pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
340 let resizable = resizable.into();
341 self.resize = self.resize.resizable(resizable);
342 self
343 }
344
345 #[inline]
347 pub fn collapsible(mut self, collapsible: bool) -> Self {
348 self.collapsible = collapsible;
349 self
350 }
351
352 #[inline]
355 pub fn title_bar(mut self, title_bar: bool) -> Self {
356 self.with_title_bar = title_bar;
357 self
358 }
359
360 #[inline]
364 pub fn auto_sized(mut self) -> Self {
365 self.resize = self.resize.auto_sized();
366 self.scroll = ScrollArea::neither();
367 self
368 }
369
370 #[inline]
374 pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
375 self.scroll = self.scroll.scroll(scroll);
376 self
377 }
378
379 #[inline]
381 pub fn hscroll(mut self, hscroll: bool) -> Self {
382 self.scroll = self.scroll.hscroll(hscroll);
383 self
384 }
385
386 #[inline]
388 pub fn vscroll(mut self, vscroll: bool) -> Self {
389 self.scroll = self.scroll.vscroll(vscroll);
390 self
391 }
392
393 #[inline]
397 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
398 self.scroll = self.scroll.scroll_source(ScrollSource {
399 drag: drag_to_scroll,
400 ..Default::default()
401 });
402 self
403 }
404
405 #[inline]
407 pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
408 self.scroll = self.scroll.scroll_bar_visibility(visibility);
409 self
410 }
411}
412
413impl Window<'_> {
414 #[inline]
417 pub fn show<R>(
418 self,
419 ctx: &Context,
420 add_contents: impl FnOnce(&mut Ui) -> R,
421 ) -> Option<InnerResponse<Option<R>>> {
422 self.show_dyn(ctx, Box::new(add_contents))
423 }
424
425 fn show_dyn<'c, R>(
426 self,
427 ctx: &Context,
428 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
429 ) -> Option<InnerResponse<Option<R>>> {
430 let Window {
431 title,
432 mut open,
433 area,
434 frame,
435 resize,
436 scroll,
437 collapsible,
438 default_open,
439 with_title_bar,
440 fade_out,
441 } = self;
442
443 let header_color =
444 frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
445 let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
446
447 let is_explicitly_closed = matches!(open, Some(false));
448 let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
449 let opacity = ctx.animate_bool_with_easing(
450 area.id.with("fade-out"),
451 is_open,
452 emath::easing::cubic_out,
453 );
454 if opacity <= 0.0 {
455 return None;
456 }
457
458 let area_id = area.id;
459 let area_layer_id = area.layer();
460 let resize_id = area_id.with("resize");
461 let mut collapsing =
462 CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
463
464 let is_collapsed = with_title_bar && !collapsing.is_open();
465 let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
466
467 let resize = resize.resizable(false); let mut resize = resize.id(resize_id);
469
470 let on_top = Some(area_layer_id) == ctx.top_layer_id();
471 let mut area = area.begin(ctx);
472
473 area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
474
475 let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
477 let style = ctx.style();
478 let title_bar_inner_height = ctx
479 .fonts(|fonts| title.font_height(fonts, &style))
480 .at_least(style.spacing.interact_size.y);
481 let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
482 let half_height = (title_bar_inner_height / 2.0).round() as _;
483 window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height);
484 window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height);
485
486 let title_content_spacing = if is_collapsed {
487 0.0
488 } else {
489 window_frame.stroke.width
490 };
491 (title_bar_inner_height, title_content_spacing)
492 } else {
493 (0.0, 0.0)
494 };
495
496 {
497 let constrain_rect = area.constrain_rect();
499 let max_width = constrain_rect.width();
500 let max_height =
501 constrain_rect.height() - title_bar_height_with_margin - title_content_spacing;
502 resize.max_size.x = resize.max_size.x.min(max_width);
503 resize.max_size.y = resize.max_size.y.min(max_height);
504 }
505
506 let last_frame_outer_rect = area.state().rect();
508 let resize_interaction = ctx.with_accessibility_parent(area.id(), || {
509 resize_interaction(
510 ctx,
511 possible,
512 area_layer_id,
513 last_frame_outer_rect,
514 window_frame,
515 )
516 });
517
518 {
519 let margins = window_frame.total_margin().sum()
520 + vec2(0.0, title_bar_height_with_margin + title_content_spacing);
521
522 resize_response(
523 resize_interaction,
524 ctx,
525 margins,
526 area_layer_id,
527 &mut area,
528 resize_id,
529 );
530 }
531
532 let mut area_content_ui = area.content_ui(ctx);
533 if is_open {
534 } else if fade_out {
537 area_content_ui.multiply_opacity(opacity);
538 }
539
540 let content_inner = {
541 ctx.with_accessibility_parent(area.id(), || {
542 let mut frame = window_frame.begin(&mut area_content_ui);
544
545 let show_close_button = open.is_some();
546
547 let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
548
549 let title_bar = if with_title_bar {
550 let title_bar = TitleBar::new(
551 &frame.content_ui,
552 title,
553 show_close_button,
554 collapsible,
555 window_frame,
556 title_bar_height_with_margin,
557 );
558 resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); frame.content_ui.set_min_size(title_bar.inner_rect.size());
561
562 if is_collapsed {
564 frame.content_ui.add_space(title_bar.inner_rect.height());
565 } else {
566 frame.content_ui.add_space(
567 title_bar.inner_rect.height()
568 + title_content_spacing
569 + window_frame.inner_margin.sum().y,
570 );
571 }
572
573 Some(title_bar)
574 } else {
575 None
576 };
577
578 let (content_inner, content_response) = collapsing
579 .show_body_unindented(&mut frame.content_ui, |ui| {
580 resize.show(ui, |ui| {
581 if scroll.is_any_scroll_enabled() {
582 scroll.show(ui, add_contents).inner
583 } else {
584 add_contents(ui)
585 }
586 })
587 })
588 .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
589
590 let outer_rect = frame.end(&mut area_content_ui).rect;
591 paint_resize_corner(
592 &area_content_ui,
593 &possible,
594 outer_rect,
595 &window_frame,
596 resize_interaction,
597 );
598
599 if let Some(mut title_bar) = title_bar {
602 title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
603 title_bar.inner_rect.max.y =
604 title_bar.inner_rect.min.y + title_bar_height_with_margin;
605
606 if on_top && area_content_ui.visuals().window_highlight_topmost {
607 let mut round =
608 window_frame.corner_radius - window_frame.stroke.width.round() as u8;
609
610 if !is_collapsed {
611 round.se = 0;
612 round.sw = 0;
613 }
614
615 area_content_ui.painter().set(
616 *where_to_put_header_background,
617 RectShape::filled(title_bar.inner_rect, round, header_color),
618 );
619 };
620
621 if false {
622 ctx.debug_painter().debug_rect(
623 title_bar.inner_rect,
624 Color32::LIGHT_BLUE,
625 "title_bar.rect",
626 );
627 }
628
629 title_bar.ui(
630 &mut area_content_ui,
631 &content_response,
632 open.as_deref_mut(),
633 &mut collapsing,
634 collapsible,
635 );
636 }
637
638 collapsing.store(ctx);
639
640 paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
641
642 content_inner
643 })
644 };
645
646 let full_response = area.end(ctx, area_content_ui);
647
648 if full_response.should_close() {
649 if let Some(open) = open {
650 *open = false;
651 }
652 }
653
654 let inner_response = InnerResponse {
655 inner: content_inner,
656 response: full_response,
657 };
658 Some(inner_response)
659 }
660}
661
662fn paint_resize_corner(
663 ui: &Ui,
664 possible: &PossibleInteractions,
665 outer_rect: Rect,
666 window_frame: &Frame,
667 i: ResizeInteraction,
668) {
669 let cr = window_frame.corner_radius;
670
671 let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
672 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
673 } else if possible.resize_left && possible.resize_bottom {
674 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
675 } else if possible.resize_left && possible.resize_top {
676 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
677 } else if possible.resize_right && possible.resize_top {
678 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
679 } else {
680 if possible.resize_right || possible.resize_bottom {
684 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
685 } else if possible.resize_left || possible.resize_bottom {
686 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
687 } else if possible.resize_left || possible.resize_top {
688 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
689 } else if possible.resize_right || possible.resize_top {
690 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
691 } else {
692 return;
693 }
694 };
695
696 let radius = radius as f32;
698 let offset =
699 ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
700
701 let stroke = if corner_response.drag {
702 ui.visuals().widgets.active.fg_stroke
703 } else if corner_response.hover {
704 ui.visuals().widgets.hovered.fg_stroke
705 } else {
706 window_frame.stroke
707 };
708
709 let fill_rect = outer_rect.shrink(window_frame.stroke.width);
710 let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
711 let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
712 let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
714}
715
716#[derive(Clone, Copy, Debug)]
720struct PossibleInteractions {
721 resize_left: bool,
723 resize_right: bool,
724 resize_top: bool,
725 resize_bottom: bool,
726}
727
728impl PossibleInteractions {
729 fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
730 let movable = area.is_enabled() && area.is_movable();
731 let resizable = resize
732 .is_resizable()
733 .and(area.is_enabled() && !is_collapsed);
734 let pivot = area.get_pivot();
735 Self {
736 resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
737 resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
738 resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
739 resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
740 }
741 }
742
743 pub fn resizable(&self) -> bool {
744 self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
745 }
746}
747
748#[derive(Clone, Copy, Debug)]
750struct ResizeInteraction {
751 outer_rect: Rect,
753
754 window_frame: Frame,
755
756 left: SideResponse,
757 right: SideResponse,
758 top: SideResponse,
759 bottom: SideResponse,
760}
761
762#[derive(Clone, Copy, Debug, Default)]
764struct SideResponse {
765 hover: bool,
766 drag: bool,
767}
768
769impl SideResponse {
770 pub fn any(&self) -> bool {
771 self.hover || self.drag
772 }
773}
774
775impl std::ops::BitAnd for SideResponse {
776 type Output = Self;
777
778 fn bitand(self, rhs: Self) -> Self::Output {
779 Self {
780 hover: self.hover && rhs.hover,
781 drag: self.drag && rhs.drag,
782 }
783 }
784}
785
786impl std::ops::BitOrAssign for SideResponse {
787 fn bitor_assign(&mut self, rhs: Self) {
788 *self = Self {
789 hover: self.hover || rhs.hover,
790 drag: self.drag || rhs.drag,
791 };
792 }
793}
794
795impl ResizeInteraction {
796 pub fn set_cursor(&self, ctx: &Context) {
797 let left = self.left.any();
798 let right = self.right.any();
799 let top = self.top.any();
800 let bottom = self.bottom.any();
801
802 if (left && top) || (right && bottom) {
804 ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
805 } else if (right && top) || (left && bottom) {
806 ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
807 } else if left || right {
808 ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
809 } else if bottom || top {
810 ctx.set_cursor_icon(CursorIcon::ResizeVertical);
811 }
812 }
813
814 pub fn any_hovered(&self) -> bool {
815 self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
816 }
817
818 pub fn any_dragged(&self) -> bool {
819 self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
820 }
821}
822
823fn resize_response(
824 resize_interaction: ResizeInteraction,
825 ctx: &Context,
826 margins: Vec2,
827 area_layer_id: LayerId,
828 area: &mut area::Prepared,
829 resize_id: Id,
830) {
831 let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
832 return;
833 };
834
835 if area.constrain() {
836 new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
837 }
838
839 area.state_mut().set_left_top_pos(new_rect.left_top());
841
842 if resize_interaction.any_dragged() {
843 if let Some(mut state) = resize::State::load(ctx, resize_id) {
844 state.requested_size = Some(new_rect.size() - margins);
845 state.store(ctx, resize_id);
846 }
847 }
848
849 ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
850}
851
852fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
854 if !interaction.any_dragged() {
855 return None;
856 }
857
858 let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
859 let mut rect = interaction.outer_rect; rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
863
864 if interaction.left.drag {
865 rect.min.x = pointer_pos.x;
866 } else if interaction.right.drag {
867 rect.max.x = pointer_pos.x;
868 }
869
870 if interaction.top.drag {
871 rect.min.y = pointer_pos.y;
872 } else if interaction.bottom.drag {
873 rect.max.y = pointer_pos.y;
874 }
875
876 rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
878
879 Some(rect.round_ui())
880}
881
882fn resize_interaction(
883 ctx: &Context,
884 possible: PossibleInteractions,
885 layer_id: LayerId,
886 outer_rect: Rect,
887 window_frame: Frame,
888) -> ResizeInteraction {
889 if !possible.resizable() {
890 return ResizeInteraction {
891 outer_rect,
892 window_frame,
893 left: Default::default(),
894 right: Default::default(),
895 top: Default::default(),
896 bottom: Default::default(),
897 };
898 }
899
900 let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
902
903 let side_response = |rect, id| {
904 let response = ctx.create_widget(
905 WidgetRect {
906 layer_id,
907 id,
908 rect,
909 interact_rect: rect,
910 sense: Sense::drag(),
911 enabled: true,
912 },
913 true,
914 );
915 SideResponse {
916 hover: response.hovered(),
917 drag: response.dragged(),
918 }
919 };
920
921 let id = Id::new(layer_id).with("edge_drag");
922
923 let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
924 let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
925
926 let vetrtical_rect = |a: Pos2, b: Pos2| {
927 Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
928 };
929 let horizontal_rect = |a: Pos2, b: Pos2| {
930 Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
931 };
932 let corner_rect =
933 |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
934
935 let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
937
938 if possible.resize_right {
942 let response = side_response(
943 vetrtical_rect(rect.right_top(), rect.right_bottom()),
944 id.with("right"),
945 );
946 right |= response;
947 }
948 if possible.resize_left {
949 let response = side_response(
950 vetrtical_rect(rect.left_top(), rect.left_bottom()),
951 id.with("left"),
952 );
953 left |= response;
954 }
955 if possible.resize_bottom {
956 let response = side_response(
957 horizontal_rect(rect.left_bottom(), rect.right_bottom()),
958 id.with("bottom"),
959 );
960 bottom |= response;
961 }
962 if possible.resize_top {
963 let response = side_response(
964 horizontal_rect(rect.left_top(), rect.right_top()),
965 id.with("top"),
966 );
967 top |= response;
968 }
969
970 if possible.resize_right || possible.resize_bottom {
979 let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
980 if possible.resize_right {
981 right |= response;
982 }
983 if possible.resize_bottom {
984 bottom |= response;
985 }
986 }
987
988 if possible.resize_right || possible.resize_top {
989 let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
990 if possible.resize_right {
991 right |= response;
992 }
993 if possible.resize_top {
994 top |= response;
995 }
996 }
997
998 if possible.resize_left || possible.resize_bottom {
999 let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
1000 if possible.resize_left {
1001 left |= response;
1002 }
1003 if possible.resize_bottom {
1004 bottom |= response;
1005 }
1006 }
1007
1008 if possible.resize_left || possible.resize_top {
1009 let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
1010 if possible.resize_left {
1011 left |= response;
1012 }
1013 if possible.resize_top {
1014 top |= response;
1015 }
1016 }
1017
1018 let interaction = ResizeInteraction {
1019 outer_rect,
1020 window_frame,
1021 left,
1022 right,
1023 top,
1024 bottom,
1025 };
1026 interaction.set_cursor(ctx);
1027 interaction
1028}
1029
1030fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
1032 use epaint::tessellator::path::add_circle_quadrant;
1033
1034 let visuals = if interaction.any_dragged() {
1035 ui.style().visuals.widgets.active
1036 } else if interaction.any_hovered() {
1037 ui.style().visuals.widgets.hovered
1038 } else {
1039 return;
1040 };
1041
1042 let [left, right, top, bottom]: [bool; 4];
1043
1044 if interaction.any_dragged() {
1045 left = interaction.left.drag;
1046 right = interaction.right.drag;
1047 top = interaction.top.drag;
1048 bottom = interaction.bottom.drag;
1049 } else {
1050 left = interaction.left.hover;
1051 right = interaction.right.hover;
1052 top = interaction.top.hover;
1053 bottom = interaction.bottom.hover;
1054 }
1055
1056 let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
1057
1058 let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1060
1061 let stroke = visuals.bg_stroke;
1063 let half_stroke = stroke.width / 2.0;
1064 let rect = rect
1065 .shrink(half_stroke)
1066 .round_to_pixels(ui.pixels_per_point())
1067 .expand(half_stroke);
1068
1069 let Rect { min, max } = rect;
1070
1071 let mut points = Vec::new();
1072
1073 if right && !bottom && !top {
1074 points.push(pos2(max.x, min.y + cr.ne));
1075 points.push(pos2(max.x, max.y - cr.se));
1076 }
1077 if right && bottom {
1078 points.push(pos2(max.x, min.y + cr.ne));
1079 points.push(pos2(max.x, max.y - cr.se));
1080 add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
1081 }
1082 if bottom {
1083 points.push(pos2(max.x - cr.se, max.y));
1084 points.push(pos2(min.x + cr.sw, max.y));
1085 }
1086 if left && bottom {
1087 add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
1088 }
1089 if left {
1090 points.push(pos2(min.x, max.y - cr.sw));
1091 points.push(pos2(min.x, min.y + cr.nw));
1092 }
1093 if left && top {
1094 add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
1095 }
1096 if top {
1097 points.push(pos2(min.x + cr.nw, min.y));
1098 points.push(pos2(max.x - cr.ne, min.y));
1099 }
1100 if right && top {
1101 add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
1102 points.push(pos2(max.x, min.y + cr.ne));
1103 points.push(pos2(max.x, max.y - cr.se));
1104 }
1105
1106 ui.painter().add(Shape::line(points, stroke));
1107}
1108
1109struct TitleBar {
1112 window_frame: Frame,
1113
1114 title_galley: Arc<Galley>,
1116
1117 inner_rect: Rect,
1122}
1123
1124impl TitleBar {
1125 fn new(
1126 ui: &Ui,
1127 title: WidgetText,
1128 show_close_button: bool,
1129 collapsible: bool,
1130 window_frame: Frame,
1131 title_bar_height_with_margin: f32,
1132 ) -> Self {
1133 if false {
1134 ui.ctx()
1135 .debug_painter()
1136 .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
1137 }
1138
1139 let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
1140
1141 let item_spacing = ui.spacing().item_spacing;
1142 let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
1143
1144 let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); let title_galley = title.into_galley(
1147 ui,
1148 Some(crate::TextWrapMode::Extend),
1149 f32::INFINITY,
1150 TextStyle::Heading,
1151 );
1152
1153 let minimum_width = if collapsible || show_close_button {
1154 2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
1156 } else {
1157 left_pad + title_galley.size().x + left_pad
1158 };
1159 let min_inner_size = vec2(minimum_width, inner_height);
1160 let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
1161
1162 if false {
1163 ui.ctx()
1164 .debug_painter()
1165 .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
1166 }
1167
1168 Self {
1169 window_frame,
1170 title_galley,
1171 inner_rect: min_rect, }
1173 }
1174
1175 fn ui(
1190 self,
1191 ui: &mut Ui,
1192 content_response: &Option<Response>,
1193 open: Option<&mut bool>,
1194 collapsing: &mut CollapsingState,
1195 collapsible: bool,
1196 ) {
1197 let window_frame = self.window_frame;
1198 let title_inner_rect = self.inner_rect;
1199
1200 if false {
1201 ui.ctx()
1202 .debug_painter()
1203 .debug_rect(self.inner_rect, Color32::RED, "TitleBar");
1204 }
1205
1206 if collapsible {
1207 let button_center = Align2::LEFT_CENTER
1209 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1210 .center();
1211 let button_size = Vec2::splat(ui.spacing().icon_width);
1212 let button_rect = Rect::from_center_size(button_center, button_size);
1213 let button_rect = button_rect.round_ui();
1214
1215 ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| {
1216 collapsing.show_default_button_with_size(ui, button_size);
1217 });
1218 }
1219
1220 if let Some(open) = open {
1221 if self.close_button_ui(ui).clicked() {
1223 *open = false;
1224 }
1225 }
1226
1227 let text_pos =
1228 emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
1229 .left_top();
1230 let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
1231 ui.painter().galley(
1232 text_pos,
1233 self.title_galley.clone(),
1234 ui.visuals().text_color(),
1235 );
1236
1237 if let Some(content_response) = &content_response {
1238 let content_rect = content_response.rect;
1240 if false {
1241 ui.ctx()
1242 .debug_painter()
1243 .debug_rect(content_rect, Color32::RED, "content_rect");
1244 }
1245 let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
1246
1247 ui.painter()
1249 .hline(title_inner_rect.x_range(), y, window_frame.stroke);
1250 }
1251
1252 let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
1254
1255 if false {
1256 ui.ctx().debug_painter().debug_rect(
1257 double_click_rect,
1258 Color32::GREEN,
1259 "double_click_rect",
1260 );
1261 }
1262
1263 let id = ui.unique_id().with("__window_title_bar");
1264
1265 if ui
1266 .interact(double_click_rect, id, Sense::click())
1267 .double_clicked()
1268 && collapsible
1269 {
1270 collapsing.toggle(ui);
1271 }
1272 }
1273
1274 fn close_button_ui(&self, ui: &mut Ui) -> Response {
1280 let button_center = Align2::RIGHT_CENTER
1281 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1282 .center();
1283 let button_size = Vec2::splat(ui.spacing().icon_width);
1284 let button_rect = Rect::from_center_size(button_center, button_size);
1285 let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
1286 close_button(ui, button_rect)
1287 }
1288}
1289
1290fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1301 let close_id = ui.auto_id_with("window_close_button");
1302 let response = ui.interact(rect, close_id, Sense::click());
1303 response
1304 .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1305
1306 ui.expand_to_include_rect(response.rect);
1307
1308 let visuals = ui.style().interact(&response);
1309 let rect = rect.shrink(2.0).expand(visuals.expansion);
1310 let stroke = visuals.fg_stroke;
1311 ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1313 ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1315 response
1316}