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 pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
75 let ViewportBuilder {
76 title,
77 app_id,
78 inner_size,
79 min_inner_size,
80 max_inner_size,
81 resizable,
82 decorations,
83 title_shown,
84 minimize_button,
85 .. } = viewport;
87
88 let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));
89
90 if let Some(inner_size) = inner_size {
91 window = window.default_size(inner_size);
92 }
93 if let Some(min_inner_size) = min_inner_size {
94 window = window.min_size(min_inner_size);
95 }
96 if let Some(max_inner_size) = max_inner_size {
97 window = window.max_size(max_inner_size);
98 }
99 if let Some(resizable) = resizable {
100 window = window.resizable(resizable);
101 }
102 window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
103 window = window.collapsible(minimize_button.unwrap_or(true));
104
105 window
106 }
107
108 #[inline]
110 pub fn id(mut self, id: Id) -> Self {
111 self.area = self.area.id(id);
112 self
113 }
114
115 #[inline]
121 pub fn open(mut self, open: &'open mut bool) -> Self {
122 self.open = Some(open);
123 self
124 }
125
126 #[inline]
128 pub fn enabled(mut self, enabled: bool) -> Self {
129 self.area = self.area.enabled(enabled);
130 self
131 }
132
133 #[inline]
139 pub fn interactable(mut self, interactable: bool) -> Self {
140 self.area = self.area.interactable(interactable);
141 self
142 }
143
144 #[inline]
146 pub fn movable(mut self, movable: bool) -> Self {
147 self.area = self.area.movable(movable);
148 self
149 }
150
151 #[inline]
153 pub fn order(mut self, order: Order) -> Self {
154 self.area = self.area.order(order);
155 self
156 }
157
158 #[inline]
162 pub fn fade_in(mut self, fade_in: bool) -> Self {
163 self.area = self.area.fade_in(fade_in);
164 self
165 }
166
167 #[inline]
173 pub fn fade_out(mut self, fade_out: bool) -> Self {
174 self.fade_out = fade_out;
175 self
176 }
177
178 #[inline]
181 pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
182 mutate(&mut self);
183 self
184 }
185
186 #[inline]
189 pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
190 self.resize = mutate(self.resize);
191 self
192 }
193
194 #[inline]
196 pub fn frame(mut self, frame: Frame) -> Self {
197 self.frame = Some(frame);
198 self
199 }
200
201 #[inline]
203 pub fn min_width(mut self, min_width: f32) -> Self {
204 self.resize = self.resize.min_width(min_width);
205 self
206 }
207
208 #[inline]
210 pub fn min_height(mut self, min_height: f32) -> Self {
211 self.resize = self.resize.min_height(min_height);
212 self
213 }
214
215 #[inline]
217 pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
218 self.resize = self.resize.min_size(min_size);
219 self
220 }
221
222 #[inline]
224 pub fn max_width(mut self, max_width: f32) -> Self {
225 self.resize = self.resize.max_width(max_width);
226 self
227 }
228
229 #[inline]
231 pub fn max_height(mut self, max_height: f32) -> Self {
232 self.resize = self.resize.max_height(max_height);
233 self
234 }
235
236 #[inline]
238 pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
239 self.resize = self.resize.max_size(max_size);
240 self
241 }
242
243 #[inline]
246 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
247 self.area = self.area.current_pos(current_pos);
248 self
249 }
250
251 #[inline]
253 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
254 self.area = self.area.default_pos(default_pos);
255 self
256 }
257
258 #[inline]
260 pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
261 self.area = self.area.fixed_pos(pos);
262 self
263 }
264
265 #[inline]
271 pub fn constrain(mut self, constrain: bool) -> Self {
272 self.area = self.area.constrain(constrain);
273 self
274 }
275
276 #[inline]
280 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
281 self.area = self.area.constrain_to(constrain_rect);
282 self
283 }
284
285 #[inline]
293 pub fn pivot(mut self, pivot: Align2) -> Self {
294 self.area = self.area.pivot(pivot);
295 self
296 }
297
298 #[inline]
310 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
311 self.area = self.area.anchor(align, offset);
312 self
313 }
314
315 #[inline]
317 pub fn default_open(mut self, default_open: bool) -> Self {
318 self.default_open = default_open;
319 self
320 }
321
322 #[inline]
324 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
325 let default_size: Vec2 = default_size.into();
326 self.resize = self.resize.default_size(default_size);
327 self.area = self.area.default_size(default_size);
328 self
329 }
330
331 #[inline]
333 pub fn default_width(mut self, default_width: f32) -> Self {
334 self.resize = self.resize.default_width(default_width);
335 self.area = self.area.default_width(default_width);
336 self
337 }
338
339 #[inline]
341 pub fn default_height(mut self, default_height: f32) -> Self {
342 self.resize = self.resize.default_height(default_height);
343 self.area = self.area.default_height(default_height);
344 self
345 }
346
347 #[inline]
349 pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
350 self.resize = self.resize.fixed_size(size);
351 self
352 }
353
354 pub fn default_rect(self, rect: Rect) -> Self {
356 self.default_pos(rect.min).default_size(rect.size())
357 }
358
359 pub fn fixed_rect(self, rect: Rect) -> Self {
361 self.fixed_pos(rect.min).fixed_size(rect.size())
362 }
363
364 #[inline]
374 pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
375 let resizable = resizable.into();
376 self.resize = self.resize.resizable(resizable);
377 self
378 }
379
380 #[inline]
382 pub fn collapsible(mut self, collapsible: bool) -> Self {
383 self.collapsible = collapsible;
384 self
385 }
386
387 #[inline]
390 pub fn title_bar(mut self, title_bar: bool) -> Self {
391 self.with_title_bar = title_bar;
392 self
393 }
394
395 #[inline]
399 pub fn auto_sized(mut self) -> Self {
400 self.resize = self.resize.auto_sized();
401 self.scroll = ScrollArea::neither();
402 self
403 }
404
405 #[inline]
409 pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
410 self.scroll = self.scroll.scroll(scroll);
411 self
412 }
413
414 #[inline]
416 pub fn hscroll(mut self, hscroll: bool) -> Self {
417 self.scroll = self.scroll.hscroll(hscroll);
418 self
419 }
420
421 #[inline]
423 pub fn vscroll(mut self, vscroll: bool) -> Self {
424 self.scroll = self.scroll.vscroll(vscroll);
425 self
426 }
427
428 #[inline]
432 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
433 self.scroll = self.scroll.scroll_source(ScrollSource {
434 drag: drag_to_scroll,
435 ..Default::default()
436 });
437 self
438 }
439
440 #[inline]
442 pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
443 self.scroll = self.scroll.scroll_bar_visibility(visibility);
444 self
445 }
446}
447
448impl Window<'_> {
449 #[inline]
452 pub fn show<R>(
453 self,
454 ctx: &Context,
455 add_contents: impl FnOnce(&mut Ui) -> R,
456 ) -> Option<InnerResponse<Option<R>>> {
457 self.show_dyn(ctx, Box::new(add_contents))
458 }
459
460 fn show_dyn<'c, R>(
461 self,
462 ctx: &Context,
463 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
464 ) -> Option<InnerResponse<Option<R>>> {
465 let Window {
466 title,
467 mut open,
468 area,
469 frame,
470 resize,
471 scroll,
472 collapsible,
473 default_open,
474 with_title_bar,
475 fade_out,
476 } = self;
477
478 let style = ctx.global_style();
479
480 let header_color =
481 frame.map_or_else(|| style.visuals.widgets.open.weak_bg_fill, |f| f.fill);
482 let mut window_frame = frame.unwrap_or_else(|| Frame::window(&style));
483
484 let is_explicitly_closed = matches!(open, Some(false));
485 let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
486 let opacity = ctx.animate_bool_with_easing(
487 area.id.with("fade-out"),
488 is_open,
489 emath::easing::cubic_out,
490 );
491 if opacity <= 0.0 {
492 return None;
493 }
494
495 let area_id = area.id;
496 let area_layer_id = area.layer();
497 let resize_id = area_id.with("resize");
498 let mut collapsing =
499 CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
500
501 let is_collapsed = with_title_bar && !collapsing.is_open();
502 let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
503
504 let resize = resize.resizable(false); let mut resize = resize.id(resize_id);
506
507 let on_top = Some(area_layer_id) == ctx.top_layer_id();
508 let mut area = area.begin(ctx);
509
510 area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
511
512 let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
514 let title_bar_inner_height = ctx
515 .fonts_mut(|fonts| title.font_height(fonts, &style))
516 .at_least(style.spacing.interact_size.y);
517 let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
518 let half_height = (title_bar_inner_height / 2.0).round() as _;
519 window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height);
520 window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height);
521
522 let title_content_spacing = if is_collapsed {
523 0.0
524 } else {
525 window_frame.stroke.width
526 };
527 (title_bar_inner_height, title_content_spacing)
528 } else {
529 (0.0, 0.0)
530 };
531
532 {
533 let constrain_rect = area.constrain_rect();
535 let max_width = constrain_rect.width();
536 let max_height =
537 constrain_rect.height() - title_bar_height_with_margin - title_content_spacing;
538 resize.max_size.x = resize.max_size.x.min(max_width);
539 resize.max_size.y = resize.max_size.y.min(max_height);
540 }
541
542 let last_frame_outer_rect = area.state().rect();
544 let resize_interaction = do_resize_interaction(
545 ctx,
546 possible,
547 area.id(),
548 area_layer_id,
549 last_frame_outer_rect,
550 window_frame,
551 );
552
553 {
554 let margins = window_frame.total_margin().sum()
555 + vec2(0.0, title_bar_height_with_margin + title_content_spacing);
556
557 resize_response(
558 resize_interaction,
559 ctx,
560 margins,
561 area_layer_id,
562 &mut area,
563 resize_id,
564 );
565 }
566
567 let mut area_content_ui = area.content_ui(ctx);
568 if is_open {
569 } else if fade_out {
572 area_content_ui.multiply_opacity(opacity);
573 }
574
575 let content_inner = {
576 let mut frame = window_frame.begin(&mut area_content_ui);
578
579 let show_close_button = open.is_some();
580
581 let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
582
583 let title_bar = if with_title_bar {
584 let title_bar = TitleBar::new(
585 &frame.content_ui,
586 title,
587 show_close_button,
588 collapsible,
589 window_frame,
590 title_bar_height_with_margin,
591 );
592 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());
595
596 if is_collapsed {
598 frame.content_ui.add_space(title_bar.inner_rect.height());
599 } else {
600 frame.content_ui.add_space(
601 title_bar.inner_rect.height()
602 + title_content_spacing
603 + window_frame.inner_margin.sum().y,
604 );
605 }
606
607 Some(title_bar)
608 } else {
609 None
610 };
611
612 let (content_inner, content_response) = collapsing
613 .show_body_unindented(&mut frame.content_ui, |ui| {
614 resize.show(ui, |ui| {
615 if scroll.is_any_scroll_enabled() {
616 scroll.show(ui, add_contents).inner
617 } else {
618 add_contents(ui)
619 }
620 })
621 })
622 .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
623
624 let outer_rect = frame.end(&mut area_content_ui).rect;
625
626 let resize_interaction = do_resize_interaction(
628 ctx,
629 possible,
630 area.id(),
631 area_layer_id,
632 last_frame_outer_rect,
633 window_frame,
634 );
635
636 paint_resize_corner(
637 &area_content_ui,
638 &possible,
639 outer_rect,
640 &window_frame,
641 resize_interaction,
642 );
643
644 if let Some(mut title_bar) = title_bar {
647 title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
648 title_bar.inner_rect.max.y =
649 title_bar.inner_rect.min.y + title_bar_height_with_margin;
650
651 if on_top && area_content_ui.visuals().window_highlight_topmost {
652 let mut round =
653 window_frame.corner_radius - window_frame.stroke.width.round() as u8;
654
655 if !is_collapsed {
656 round.se = 0;
657 round.sw = 0;
658 }
659
660 area_content_ui.painter().set(
661 *where_to_put_header_background,
662 RectShape::filled(title_bar.inner_rect, round, header_color),
663 );
664 }
665
666 if false {
667 ctx.debug_painter().debug_rect(
668 title_bar.inner_rect,
669 Color32::LIGHT_BLUE,
670 "title_bar.rect",
671 );
672 }
673
674 title_bar.ui(
675 &mut area_content_ui,
676 &content_response,
677 open.as_deref_mut(),
678 &mut collapsing,
679 collapsible,
680 );
681 }
682
683 collapsing.store(ctx);
684
685 paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
686
687 content_inner
688 };
689
690 let full_response = area.end(ctx, area_content_ui);
691
692 if full_response.should_close()
693 && let Some(open) = open
694 {
695 *open = false;
696 }
697
698 let inner_response = InnerResponse {
699 inner: content_inner,
700 response: full_response,
701 };
702 Some(inner_response)
703 }
704}
705
706fn paint_resize_corner(
707 ui: &Ui,
708 possible: &PossibleInteractions,
709 outer_rect: Rect,
710 window_frame: &Frame,
711 i: ResizeInteraction,
712) {
713 let cr = window_frame.corner_radius;
714
715 let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
716 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
717 } else if possible.resize_left && possible.resize_bottom {
718 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
719 } else if possible.resize_left && possible.resize_top {
720 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
721 } else if possible.resize_right && possible.resize_top {
722 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
723 } else {
724 if possible.resize_right || possible.resize_bottom {
728 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
729 } else if possible.resize_left || possible.resize_bottom {
730 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
731 } else if possible.resize_left || possible.resize_top {
732 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
733 } else if possible.resize_right || possible.resize_top {
734 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
735 } else {
736 return;
737 }
738 };
739
740 let radius = radius as f32;
742 let offset =
743 ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
744
745 let stroke = if corner_response.drag {
746 ui.visuals().widgets.active.fg_stroke
747 } else if corner_response.hover {
748 ui.visuals().widgets.hovered.fg_stroke
749 } else {
750 window_frame.stroke
751 };
752
753 let fill_rect = outer_rect.shrink(window_frame.stroke.width);
754 let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
755 let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
756 let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
758}
759
760#[derive(Clone, Copy, Debug)]
764struct PossibleInteractions {
765 resize_left: bool,
767 resize_right: bool,
768 resize_top: bool,
769 resize_bottom: bool,
770}
771
772impl PossibleInteractions {
773 fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
774 let movable = area.is_enabled() && area.is_movable();
775 let resizable = resize
776 .is_resizable()
777 .and(area.is_enabled() && !is_collapsed);
778 let pivot = area.get_pivot();
779 Self {
780 resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
781 resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
782 resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
783 resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
784 }
785 }
786
787 pub fn resizable(&self) -> bool {
788 self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
789 }
790}
791
792#[derive(Clone, Copy, Debug)]
794struct ResizeInteraction {
795 outer_rect: Rect,
797
798 window_frame: Frame,
799
800 left: SideResponse,
801 right: SideResponse,
802 top: SideResponse,
803 bottom: SideResponse,
804}
805
806#[derive(Clone, Copy, Debug, Default)]
808struct SideResponse {
809 hover: bool,
810 drag: bool,
811}
812
813impl SideResponse {
814 pub fn any(&self) -> bool {
815 self.hover || self.drag
816 }
817}
818
819impl std::ops::BitAnd for SideResponse {
820 type Output = Self;
821
822 fn bitand(self, rhs: Self) -> Self::Output {
823 Self {
824 hover: self.hover && rhs.hover,
825 drag: self.drag && rhs.drag,
826 }
827 }
828}
829
830impl std::ops::BitOrAssign for SideResponse {
831 fn bitor_assign(&mut self, rhs: Self) {
832 *self = Self {
833 hover: self.hover || rhs.hover,
834 drag: self.drag || rhs.drag,
835 };
836 }
837}
838
839impl ResizeInteraction {
840 pub fn set_cursor(&self, ctx: &Context) {
841 let left = self.left.any();
842 let right = self.right.any();
843 let top = self.top.any();
844 let bottom = self.bottom.any();
845
846 if (left && top) || (right && bottom) {
848 ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
849 } else if (right && top) || (left && bottom) {
850 ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
851 } else if left || right {
852 ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
853 } else if bottom || top {
854 ctx.set_cursor_icon(CursorIcon::ResizeVertical);
855 }
856 }
857
858 pub fn any_hovered(&self) -> bool {
859 self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
860 }
861
862 pub fn any_dragged(&self) -> bool {
863 self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
864 }
865}
866
867fn resize_response(
868 resize_interaction: ResizeInteraction,
869 ctx: &Context,
870 margins: Vec2,
871 area_layer_id: LayerId,
872 area: &mut area::Prepared,
873 resize_id: Id,
874) {
875 let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
876 return;
877 };
878
879 if area.constrain() {
880 new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
881 }
882
883 area.state_mut().set_left_top_pos(new_rect.left_top());
885
886 if resize_interaction.any_dragged()
887 && let Some(mut state) = resize::State::load(ctx, resize_id)
888 {
889 state.requested_size = Some(new_rect.size() - margins);
890 state.store(ctx, resize_id);
891 }
892
893 ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
894}
895
896fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
898 let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
900
901 if !interaction.any_dragged() {
902 ctx.data_mut(|data| {
903 data.remove::<Rect>(rect_at_start_of_drag_id);
904 });
905 return None;
906 }
907
908 let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
909
910 let rect_at_start_of_drag = ctx.data_mut(|data| {
911 *data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
912 });
913
914 let mut rect = rect_at_start_of_drag; rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
918
919 if interaction.left.drag {
920 rect.min.x += total_drag_delta.x;
921 } else if interaction.right.drag {
922 rect.max.x += total_drag_delta.x;
923 }
924
925 if interaction.top.drag {
926 rect.min.y += total_drag_delta.y;
927 } else if interaction.bottom.drag {
928 rect.max.y += total_drag_delta.y;
929 }
930
931 rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
933
934 Some(rect.round_ui())
935}
936
937fn do_resize_interaction(
938 ctx: &Context,
939 possible: PossibleInteractions,
940 accessibility_parent: Id,
941 layer_id: LayerId,
942 outer_rect: Rect,
943 window_frame: Frame,
944) -> ResizeInteraction {
945 if !possible.resizable() {
946 return ResizeInteraction {
947 outer_rect,
948 window_frame,
949 left: Default::default(),
950 right: Default::default(),
951 top: Default::default(),
952 bottom: Default::default(),
953 };
954 }
955
956 let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
958
959 let side_response = |rect, id| {
960 ctx.register_accesskit_parent(id, accessibility_parent);
961 let response = ctx.create_widget(
962 WidgetRect {
963 layer_id,
964 id,
965 parent_id: layer_id.id,
966 rect,
967 interact_rect: rect,
968 sense: Sense::DRAG, enabled: true,
970 },
971 true,
972 InteractOptions {
973 move_to_top: true,
977 },
978 );
979
980 response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle));
981
982 SideResponse {
983 hover: response.hovered(),
984 drag: response.dragged(),
985 }
986 };
987
988 let id = Id::new(layer_id).with("edge_drag");
989
990 let style = ctx.global_style();
991
992 let side_grab_radius = style.interaction.resize_grab_radius_side;
993 let corner_grab_radius = style.interaction.resize_grab_radius_corner;
994
995 let vetrtical_rect = |a: Pos2, b: Pos2| {
996 Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
997 };
998 let horizontal_rect = |a: Pos2, b: Pos2| {
999 Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
1000 };
1001 let corner_rect =
1002 |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
1003
1004 let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
1006
1007 if possible.resize_right {
1011 let response = side_response(
1012 vetrtical_rect(rect.right_top(), rect.right_bottom()),
1013 id.with("right"),
1014 );
1015 right |= response;
1016 }
1017 if possible.resize_left {
1018 let response = side_response(
1019 vetrtical_rect(rect.left_top(), rect.left_bottom()),
1020 id.with("left"),
1021 );
1022 left |= response;
1023 }
1024 if possible.resize_bottom {
1025 let response = side_response(
1026 horizontal_rect(rect.left_bottom(), rect.right_bottom()),
1027 id.with("bottom"),
1028 );
1029 bottom |= response;
1030 }
1031 if possible.resize_top {
1032 let response = side_response(
1033 horizontal_rect(rect.left_top(), rect.right_top()),
1034 id.with("top"),
1035 );
1036 top |= response;
1037 }
1038
1039 if possible.resize_right || possible.resize_bottom {
1048 let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
1049 if possible.resize_right {
1050 right |= response;
1051 }
1052 if possible.resize_bottom {
1053 bottom |= response;
1054 }
1055 }
1056
1057 if possible.resize_right || possible.resize_top {
1058 let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
1059 if possible.resize_right {
1060 right |= response;
1061 }
1062 if possible.resize_top {
1063 top |= response;
1064 }
1065 }
1066
1067 if possible.resize_left || possible.resize_bottom {
1068 let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
1069 if possible.resize_left {
1070 left |= response;
1071 }
1072 if possible.resize_bottom {
1073 bottom |= response;
1074 }
1075 }
1076
1077 if possible.resize_left || possible.resize_top {
1078 let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
1079 if possible.resize_left {
1080 left |= response;
1081 }
1082 if possible.resize_top {
1083 top |= response;
1084 }
1085 }
1086
1087 let interaction = ResizeInteraction {
1088 outer_rect,
1089 window_frame,
1090 left,
1091 right,
1092 top,
1093 bottom,
1094 };
1095 interaction.set_cursor(ctx);
1096 interaction
1097}
1098
1099fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
1101 use epaint::tessellator::path::add_circle_quadrant;
1102
1103 let visuals = if interaction.any_dragged() {
1104 ui.style().visuals.widgets.active
1105 } else if interaction.any_hovered() {
1106 ui.style().visuals.widgets.hovered
1107 } else {
1108 return;
1109 };
1110
1111 let [left, right, top, bottom]: [bool; 4];
1112
1113 if interaction.any_dragged() {
1114 left = interaction.left.drag;
1115 right = interaction.right.drag;
1116 top = interaction.top.drag;
1117 bottom = interaction.bottom.drag;
1118 } else {
1119 left = interaction.left.hover;
1120 right = interaction.right.hover;
1121 top = interaction.top.hover;
1122 bottom = interaction.bottom.hover;
1123 }
1124
1125 let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
1126
1127 let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1129
1130 let stroke = visuals.bg_stroke;
1132 let half_stroke = stroke.width / 2.0;
1133 let rect = rect
1134 .shrink(half_stroke)
1135 .round_to_pixels(ui.pixels_per_point())
1136 .expand(half_stroke);
1137
1138 let Rect { min, max } = rect;
1139
1140 let mut points = Vec::new();
1141
1142 if right && !bottom && !top {
1143 points.push(pos2(max.x, min.y + cr.ne));
1144 points.push(pos2(max.x, max.y - cr.se));
1145 }
1146 if right && bottom {
1147 points.push(pos2(max.x, min.y + cr.ne));
1148 points.push(pos2(max.x, max.y - cr.se));
1149 add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
1150 }
1151 if bottom {
1152 points.push(pos2(max.x - cr.se, max.y));
1153 points.push(pos2(min.x + cr.sw, max.y));
1154 }
1155 if left && bottom {
1156 add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
1157 }
1158 if left {
1159 points.push(pos2(min.x, max.y - cr.sw));
1160 points.push(pos2(min.x, min.y + cr.nw));
1161 }
1162 if left && top {
1163 add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
1164 }
1165 if top {
1166 points.push(pos2(min.x + cr.nw, min.y));
1167 points.push(pos2(max.x - cr.ne, min.y));
1168 }
1169 if right && top {
1170 add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
1171 points.push(pos2(max.x, min.y + cr.ne));
1172 points.push(pos2(max.x, max.y - cr.se));
1173 }
1174
1175 ui.painter().add(Shape::line(points, stroke));
1176}
1177
1178struct TitleBar {
1181 window_frame: Frame,
1182
1183 title_galley: Arc<Galley>,
1185
1186 inner_rect: Rect,
1191}
1192
1193impl TitleBar {
1194 fn new(
1195 ui: &Ui,
1196 title: WidgetText,
1197 show_close_button: bool,
1198 collapsible: bool,
1199 window_frame: Frame,
1200 title_bar_height_with_margin: f32,
1201 ) -> Self {
1202 if false {
1203 ui.debug_painter()
1204 .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
1205 }
1206
1207 let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
1208
1209 let item_spacing = ui.spacing().item_spacing;
1210 let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
1211
1212 let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); let title_galley = title.into_galley(
1215 ui,
1216 Some(crate::TextWrapMode::Extend),
1217 f32::INFINITY,
1218 TextStyle::Heading,
1219 );
1220
1221 let minimum_width = if collapsible || show_close_button {
1222 2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
1224 } else {
1225 left_pad + title_galley.size().x + left_pad
1226 };
1227 let min_inner_size = vec2(minimum_width, inner_height);
1228 let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
1229
1230 if false {
1231 ui.debug_painter()
1232 .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
1233 }
1234
1235 Self {
1236 window_frame,
1237 title_galley,
1238 inner_rect: min_rect, }
1240 }
1241
1242 fn ui(
1257 self,
1258 ui: &mut Ui,
1259 content_response: &Option<Response>,
1260 open: Option<&mut bool>,
1261 collapsing: &mut CollapsingState,
1262 collapsible: bool,
1263 ) {
1264 let window_frame = self.window_frame;
1265 let title_inner_rect = self.inner_rect;
1266
1267 if false {
1268 ui.debug_painter()
1269 .debug_rect(self.inner_rect, Color32::RED, "TitleBar");
1270 }
1271
1272 if collapsible {
1273 let button_center = Align2::LEFT_CENTER
1275 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1276 .center();
1277 let button_size = Vec2::splat(ui.spacing().icon_width);
1278 let button_rect = Rect::from_center_size(button_center, button_size);
1279 let button_rect = button_rect.round_ui();
1280
1281 ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| {
1282 collapsing.show_default_button_with_size(ui, button_size);
1283 });
1284 }
1285
1286 if let Some(open) = open {
1287 if self.close_button_ui(ui).clicked() {
1289 *open = false;
1290 }
1291 }
1292
1293 let text_pos =
1294 emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
1295 .left_top();
1296 let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
1297 ui.painter().galley(
1298 text_pos,
1299 Arc::clone(&self.title_galley),
1300 ui.visuals().text_color(),
1301 );
1302
1303 if let Some(content_response) = &content_response {
1304 let content_rect = content_response.rect;
1306 if false {
1307 ui.debug_painter()
1308 .debug_rect(content_rect, Color32::RED, "content_rect");
1309 }
1310 let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
1311
1312 ui.painter()
1314 .hline(title_inner_rect.x_range(), y, window_frame.stroke);
1315 }
1316
1317 let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
1319
1320 if false {
1321 ui.debug_painter()
1322 .debug_rect(double_click_rect, Color32::GREEN, "double_click_rect");
1323 }
1324
1325 let id = ui.unique_id().with("__window_title_bar");
1326
1327 if ui
1328 .interact(double_click_rect, id, Sense::CLICK)
1329 .double_clicked()
1330 && collapsible
1331 {
1332 collapsing.toggle(ui);
1333 }
1334 }
1335
1336 fn close_button_ui(&self, ui: &mut Ui) -> Response {
1342 let button_center = Align2::RIGHT_CENTER
1343 .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1344 .center();
1345 let button_size = Vec2::splat(ui.spacing().icon_width);
1346 let button_rect = Rect::from_center_size(button_center, button_size);
1347 let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
1348 close_button(ui, button_rect)
1349 }
1350}
1351
1352fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1363 let close_id = ui.auto_id_with("window_close_button");
1364 let response = ui.interact(rect, close_id, Sense::click());
1365 response
1366 .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1367
1368 ui.expand_to_include_rect(response.rect);
1369
1370 let visuals = ui.style().interact(&response);
1371 let rect = rect.shrink(2.0).expand(visuals.expansion);
1372 let stroke = visuals.fg_stroke;
1373 ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1375 ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1377 response
1378}