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_mut(|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 = resize_interaction(
509 ctx,
510 possible,
511 area.id(),
512 area_layer_id,
513 last_frame_outer_rect,
514 window_frame,
515 );
516
517 {
518 let margins = window_frame.total_margin().sum()
519 + vec2(0.0, title_bar_height_with_margin + title_content_spacing);
520
521 resize_response(
522 resize_interaction,
523 ctx,
524 margins,
525 area_layer_id,
526 &mut area,
527 resize_id,
528 );
529 }
530
531 let mut area_content_ui = area.content_ui(ctx);
532 if is_open {
533 } else if fade_out {
536 area_content_ui.multiply_opacity(opacity);
537 }
538
539 let content_inner = {
540 let mut frame = window_frame.begin(&mut area_content_ui);
542
543 let show_close_button = open.is_some();
544
545 let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
546
547 let title_bar = if with_title_bar {
548 let title_bar = TitleBar::new(
549 &frame.content_ui,
550 title,
551 show_close_button,
552 collapsible,
553 window_frame,
554 title_bar_height_with_margin,
555 );
556 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());
559
560 if is_collapsed {
562 frame.content_ui.add_space(title_bar.inner_rect.height());
563 } else {
564 frame.content_ui.add_space(
565 title_bar.inner_rect.height()
566 + title_content_spacing
567 + window_frame.inner_margin.sum().y,
568 );
569 }
570
571 Some(title_bar)
572 } else {
573 None
574 };
575
576 let (content_inner, content_response) = collapsing
577 .show_body_unindented(&mut frame.content_ui, |ui| {
578 resize.show(ui, |ui| {
579 if scroll.is_any_scroll_enabled() {
580 scroll.show(ui, add_contents).inner
581 } else {
582 add_contents(ui)
583 }
584 })
585 })
586 .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
587
588 let outer_rect = frame.end(&mut area_content_ui).rect;
589 paint_resize_corner(
590 &area_content_ui,
591 &possible,
592 outer_rect,
593 &window_frame,
594 resize_interaction,
595 );
596
597 if let Some(mut title_bar) = title_bar {
600 title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
601 title_bar.inner_rect.max.y =
602 title_bar.inner_rect.min.y + title_bar_height_with_margin;
603
604 if on_top && area_content_ui.visuals().window_highlight_topmost {
605 let mut round =
606 window_frame.corner_radius - window_frame.stroke.width.round() as u8;
607
608 if !is_collapsed {
609 round.se = 0;
610 round.sw = 0;
611 }
612
613 area_content_ui.painter().set(
614 *where_to_put_header_background,
615 RectShape::filled(title_bar.inner_rect, round, header_color),
616 );
617 }
618
619 if false {
620 ctx.debug_painter().debug_rect(
621 title_bar.inner_rect,
622 Color32::LIGHT_BLUE,
623 "title_bar.rect",
624 );
625 }
626
627 title_bar.ui(
628 &mut area_content_ui,
629 &content_response,
630 open.as_deref_mut(),
631 &mut collapsing,
632 collapsible,
633 );
634 }
635
636 collapsing.store(ctx);
637
638 paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
639
640 content_inner
641 };
642
643 let full_response = area.end(ctx, area_content_ui);
644
645 if full_response.should_close()
646 && let Some(open) = open
647 {
648 *open = false;
649 }
650
651 let inner_response = InnerResponse {
652 inner: content_inner,
653 response: full_response,
654 };
655 Some(inner_response)
656 }
657}
658
659fn paint_resize_corner(
660 ui: &Ui,
661 possible: &PossibleInteractions,
662 outer_rect: Rect,
663 window_frame: &Frame,
664 i: ResizeInteraction,
665) {
666 let cr = window_frame.corner_radius;
667
668 let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
669 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
670 } else if possible.resize_left && possible.resize_bottom {
671 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
672 } else if possible.resize_left && possible.resize_top {
673 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
674 } else if possible.resize_right && possible.resize_top {
675 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
676 } else {
677 if possible.resize_right || possible.resize_bottom {
681 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
682 } else if possible.resize_left || possible.resize_bottom {
683 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
684 } else if possible.resize_left || possible.resize_top {
685 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
686 } else if possible.resize_right || possible.resize_top {
687 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
688 } else {
689 return;
690 }
691 };
692
693 let radius = radius as f32;
695 let offset =
696 ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
697
698 let stroke = if corner_response.drag {
699 ui.visuals().widgets.active.fg_stroke
700 } else if corner_response.hover {
701 ui.visuals().widgets.hovered.fg_stroke
702 } else {
703 window_frame.stroke
704 };
705
706 let fill_rect = outer_rect.shrink(window_frame.stroke.width);
707 let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
708 let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
709 let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
711}
712
713#[derive(Clone, Copy, Debug)]
717struct PossibleInteractions {
718 resize_left: bool,
720 resize_right: bool,
721 resize_top: bool,
722 resize_bottom: bool,
723}
724
725impl PossibleInteractions {
726 fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
727 let movable = area.is_enabled() && area.is_movable();
728 let resizable = resize
729 .is_resizable()
730 .and(area.is_enabled() && !is_collapsed);
731 let pivot = area.get_pivot();
732 Self {
733 resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
734 resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
735 resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
736 resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
737 }
738 }
739
740 pub fn resizable(&self) -> bool {
741 self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
742 }
743}
744
745#[derive(Clone, Copy, Debug)]
747struct ResizeInteraction {
748 outer_rect: Rect,
750
751 window_frame: Frame,
752
753 left: SideResponse,
754 right: SideResponse,
755 top: SideResponse,
756 bottom: SideResponse,
757}
758
759#[derive(Clone, Copy, Debug, Default)]
761struct SideResponse {
762 hover: bool,
763 drag: bool,
764}
765
766impl SideResponse {
767 pub fn any(&self) -> bool {
768 self.hover || self.drag
769 }
770}
771
772impl std::ops::BitAnd for SideResponse {
773 type Output = Self;
774
775 fn bitand(self, rhs: Self) -> Self::Output {
776 Self {
777 hover: self.hover && rhs.hover,
778 drag: self.drag && rhs.drag,
779 }
780 }
781}
782
783impl std::ops::BitOrAssign for SideResponse {
784 fn bitor_assign(&mut self, rhs: Self) {
785 *self = Self {
786 hover: self.hover || rhs.hover,
787 drag: self.drag || rhs.drag,
788 };
789 }
790}
791
792impl ResizeInteraction {
793 pub fn set_cursor(&self, ctx: &Context) {
794 let left = self.left.any();
795 let right = self.right.any();
796 let top = self.top.any();
797 let bottom = self.bottom.any();
798
799 if (left && top) || (right && bottom) {
801 ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
802 } else if (right && top) || (left && bottom) {
803 ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
804 } else if left || right {
805 ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
806 } else if bottom || top {
807 ctx.set_cursor_icon(CursorIcon::ResizeVertical);
808 }
809 }
810
811 pub fn any_hovered(&self) -> bool {
812 self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
813 }
814
815 pub fn any_dragged(&self) -> bool {
816 self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
817 }
818}
819
820fn resize_response(
821 resize_interaction: ResizeInteraction,
822 ctx: &Context,
823 margins: Vec2,
824 area_layer_id: LayerId,
825 area: &mut area::Prepared,
826 resize_id: Id,
827) {
828 let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
829 return;
830 };
831
832 if area.constrain() {
833 new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
834 }
835
836 area.state_mut().set_left_top_pos(new_rect.left_top());
838
839 if resize_interaction.any_dragged()
840 && let Some(mut state) = resize::State::load(ctx, resize_id)
841 {
842 state.requested_size = Some(new_rect.size() - margins);
843 state.store(ctx, resize_id);
844 }
845
846 ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
847}
848
849fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
851 let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
853
854 if !interaction.any_dragged() {
855 ctx.data_mut(|data| {
856 data.remove::<Rect>(rect_at_start_of_drag_id);
857 });
858 return None;
859 }
860
861 let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
862
863 let rect_at_start_of_drag = ctx.data_mut(|data| {
864 *data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
865 });
866
867 let mut rect = rect_at_start_of_drag; rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
871
872 if interaction.left.drag {
873 rect.min.x += total_drag_delta.x;
874 } else if interaction.right.drag {
875 rect.max.x += total_drag_delta.x;
876 }
877
878 if interaction.top.drag {
879 rect.min.y += total_drag_delta.y;
880 } else if interaction.bottom.drag {
881 rect.max.y += total_drag_delta.y;
882 }
883
884 rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
886
887 Some(rect.round_ui())
888}
889
890fn resize_interaction(
891 ctx: &Context,
892 possible: PossibleInteractions,
893 _accessibility_parent: Id,
894 layer_id: LayerId,
895 outer_rect: Rect,
896 window_frame: Frame,
897) -> ResizeInteraction {
898 if !possible.resizable() {
899 return ResizeInteraction {
900 outer_rect,
901 window_frame,
902 left: Default::default(),
903 right: Default::default(),
904 top: Default::default(),
905 bottom: Default::default(),
906 };
907 }
908
909 let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
911
912 let side_response = |rect, id| {
913 #[cfg(feature = "accesskit")]
914 ctx.register_accesskit_parent(id, _accessibility_parent);
915 let response = ctx.create_widget(
916 WidgetRect {
917 layer_id,
918 id,
919 rect,
920 interact_rect: rect,
921 sense: Sense::drag(),
922 enabled: true,
923 },
924 true,
925 );
926 SideResponse {
927 hover: response.hovered(),
928 drag: response.dragged(),
929 }
930 };
931
932 let id = Id::new(layer_id).with("edge_drag");
933
934 let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
935 let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
936
937 let vetrtical_rect = |a: Pos2, b: Pos2| {
938 Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
939 };
940 let horizontal_rect = |a: Pos2, b: Pos2| {
941 Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
942 };
943 let corner_rect =
944 |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
945
946 let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
948
949 if possible.resize_right {
953 let response = side_response(
954 vetrtical_rect(rect.right_top(), rect.right_bottom()),
955 id.with("right"),
956 );
957 right |= response;
958 }
959 if possible.resize_left {
960 let response = side_response(
961 vetrtical_rect(rect.left_top(), rect.left_bottom()),
962 id.with("left"),
963 );
964 left |= response;
965 }
966 if possible.resize_bottom {
967 let response = side_response(
968 horizontal_rect(rect.left_bottom(), rect.right_bottom()),
969 id.with("bottom"),
970 );
971 bottom |= response;
972 }
973 if possible.resize_top {
974 let response = side_response(
975 horizontal_rect(rect.left_top(), rect.right_top()),
976 id.with("top"),
977 );
978 top |= response;
979 }
980
981 if possible.resize_right || possible.resize_bottom {
990 let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
991 if possible.resize_right {
992 right |= response;
993 }
994 if possible.resize_bottom {
995 bottom |= response;
996 }
997 }
998
999 if possible.resize_right || possible.resize_top {
1000 let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
1001 if possible.resize_right {
1002 right |= response;
1003 }
1004 if possible.resize_top {
1005 top |= response;
1006 }
1007 }
1008
1009 if possible.resize_left || possible.resize_bottom {
1010 let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
1011 if possible.resize_left {
1012 left |= response;
1013 }
1014 if possible.resize_bottom {
1015 bottom |= response;
1016 }
1017 }
1018
1019 if possible.resize_left || possible.resize_top {
1020 let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
1021 if possible.resize_left {
1022 left |= response;
1023 }
1024 if possible.resize_top {
1025 top |= response;
1026 }
1027 }
1028
1029 let interaction = ResizeInteraction {
1030 outer_rect,
1031 window_frame,
1032 left,
1033 right,
1034 top,
1035 bottom,
1036 };
1037 interaction.set_cursor(ctx);
1038 interaction
1039}
1040
1041fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
1043 use epaint::tessellator::path::add_circle_quadrant;
1044
1045 let visuals = if interaction.any_dragged() {
1046 ui.style().visuals.widgets.active
1047 } else if interaction.any_hovered() {
1048 ui.style().visuals.widgets.hovered
1049 } else {
1050 return;
1051 };
1052
1053 let [left, right, top, bottom]: [bool; 4];
1054
1055 if interaction.any_dragged() {
1056 left = interaction.left.drag;
1057 right = interaction.right.drag;
1058 top = interaction.top.drag;
1059 bottom = interaction.bottom.drag;
1060 } else {
1061 left = interaction.left.hover;
1062 right = interaction.right.hover;
1063 top = interaction.top.hover;
1064 bottom = interaction.bottom.hover;
1065 }
1066
1067 let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
1068
1069 let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1071
1072 let stroke = visuals.bg_stroke;
1074 let half_stroke = stroke.width / 2.0;
1075 let rect = rect
1076 .shrink(half_stroke)
1077 .round_to_pixels(ui.pixels_per_point())
1078 .expand(half_stroke);
1079
1080 let Rect { min, max } = rect;
1081
1082 let mut points = Vec::new();
1083
1084 if right && !bottom && !top {
1085 points.push(pos2(max.x, min.y + cr.ne));
1086 points.push(pos2(max.x, max.y - cr.se));
1087 }
1088 if right && bottom {
1089 points.push(pos2(max.x, min.y + cr.ne));
1090 points.push(pos2(max.x, max.y - cr.se));
1091 add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
1092 }
1093 if bottom {
1094 points.push(pos2(max.x - cr.se, max.y));
1095 points.push(pos2(min.x + cr.sw, max.y));
1096 }
1097 if left && bottom {
1098 add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
1099 }
1100 if left {
1101 points.push(pos2(min.x, max.y - cr.sw));
1102 points.push(pos2(min.x, min.y + cr.nw));
1103 }
1104 if left && top {
1105 add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
1106 }
1107 if top {
1108 points.push(pos2(min.x + cr.nw, min.y));
1109 points.push(pos2(max.x - cr.ne, min.y));
1110 }
1111 if right && top {
1112 add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
1113 points.push(pos2(max.x, min.y + cr.ne));
1114 points.push(pos2(max.x, max.y - cr.se));
1115 }
1116
1117 ui.painter().add(Shape::line(points, stroke));
1118}
1119
1120struct TitleBar {
1123 window_frame: Frame,
1124
1125 title_galley: Arc<Galley>,
1127
1128 inner_rect: Rect,
1133}
1134
1135impl TitleBar {
1136 fn new(
1137 ui: &Ui,
1138 title: WidgetText,
1139 show_close_button: bool,
1140 collapsible: bool,
1141 window_frame: Frame,
1142 title_bar_height_with_margin: f32,
1143 ) -> Self {
1144 if false {
1145 ui.ctx()
1146 .debug_painter()
1147 .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
1148 }
1149
1150 let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
1151
1152 let item_spacing = ui.spacing().item_spacing;
1153 let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
1154
1155 let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); let title_galley = title.into_galley(
1158 ui,
1159 Some(crate::TextWrapMode::Extend),
1160 f32::INFINITY,
1161 TextStyle::Heading,
1162 );
1163
1164 let minimum_width = if collapsible || show_close_button {
1165 2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
1167 } else {
1168 left_pad + title_galley.size().x + left_pad
1169 };
1170 let min_inner_size = vec2(minimum_width, inner_height);
1171 let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
1172
1173 if false {
1174 ui.ctx()
1175 .debug_painter()
1176 .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
1177 }
1178
1179 Self {
1180 window_frame,
1181 title_galley,
1182 inner_rect: min_rect, }
1184 }
1185
1186 fn ui(
1201 self,
1202 ui: &mut Ui,
1203 content_response: &Option<Response>,
1204 open: Option<&mut bool>,
1205 collapsing: &mut CollapsingState,
1206 collapsible: bool,
1207 ) {
1208 let window_frame = self.window_frame;
1209 let title_inner_rect = self.inner_rect;
1210
1211 if false {
1212 ui.ctx()
1213 .debug_painter()
1214 .debug_rect(self.inner_rect, Color32::RED, "TitleBar");
1215 }
1216
1217 if collapsible {
1218 let button_center = Align2::LEFT_CENTER
1220 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1221 .center();
1222 let button_size = Vec2::splat(ui.spacing().icon_width);
1223 let button_rect = Rect::from_center_size(button_center, button_size);
1224 let button_rect = button_rect.round_ui();
1225
1226 ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| {
1227 collapsing.show_default_button_with_size(ui, button_size);
1228 });
1229 }
1230
1231 if let Some(open) = open {
1232 if self.close_button_ui(ui).clicked() {
1234 *open = false;
1235 }
1236 }
1237
1238 let text_pos =
1239 emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
1240 .left_top();
1241 let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
1242 ui.painter().galley(
1243 text_pos,
1244 self.title_galley.clone(),
1245 ui.visuals().text_color(),
1246 );
1247
1248 if let Some(content_response) = &content_response {
1249 let content_rect = content_response.rect;
1251 if false {
1252 ui.ctx()
1253 .debug_painter()
1254 .debug_rect(content_rect, Color32::RED, "content_rect");
1255 }
1256 let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
1257
1258 ui.painter()
1260 .hline(title_inner_rect.x_range(), y, window_frame.stroke);
1261 }
1262
1263 let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
1265
1266 if false {
1267 ui.ctx().debug_painter().debug_rect(
1268 double_click_rect,
1269 Color32::GREEN,
1270 "double_click_rect",
1271 );
1272 }
1273
1274 let id = ui.unique_id().with("__window_title_bar");
1275
1276 if ui
1277 .interact(double_click_rect, id, Sense::click())
1278 .double_clicked()
1279 && collapsible
1280 {
1281 collapsing.toggle(ui);
1282 }
1283 }
1284
1285 fn close_button_ui(&self, ui: &mut Ui) -> Response {
1291 let button_center = Align2::RIGHT_CENTER
1292 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1293 .center();
1294 let button_size = Vec2::splat(ui.spacing().icon_width);
1295 let button_rect = Rect::from_center_size(button_center, button_size);
1296 let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
1297 close_button(ui, button_rect)
1298 }
1299}
1300
1301fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1312 let close_id = ui.auto_id_with("window_close_button");
1313 let response = ui.interact(rect, close_id, Sense::click());
1314 response
1315 .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1316
1317 ui.expand_to_include_rect(response.rect);
1318
1319 let visuals = ui.style().interact(&response);
1320 let rect = rect.shrink(2.0).expand(visuals.expansion);
1321 let stroke = visuals.fg_stroke;
1322 ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1324 ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1326 response
1327}