1use emath::GuiRounding as _;
4use epaint::CornerRadiusF32;
5
6use crate::collapsing_header::CollapsingState;
7use crate::*;
8
9use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
10use super::{Area, Frame, Resize, ScrollArea, area, resize};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
17pub enum WindowDrag {
18 Off,
23
24 Anywhere,
29
30 TitleBar,
35
36 #[default]
40 OnTouch,
41}
42
43impl WindowDrag {
44 fn resolve(self, ctx: &Context) -> Self {
47 match self {
48 Self::OnTouch => {
49 if ctx.input(|i| i.has_touch_screen()) {
50 Self::Anywhere
51 } else {
52 Self::TitleBar
53 }
54 }
55 other => other,
56 }
57 }
58}
59
60#[must_use = "You should call .show()"]
82pub struct Window<'a> {
83 title: Atoms<'a>,
84 open: Option<&'a mut bool>,
85 area: Area,
86 frame: Option<Frame>,
87 title_frame: Option<Frame>,
88 resize: Resize,
89 scroll: ScrollArea,
90 collapsible: bool,
91 default_open: bool,
92 with_title_bar: bool,
93 fade_out: bool,
94 auto_sized: bool,
95 drag_area: WindowDrag,
96}
97
98impl<'a> Window<'a> {
99 pub fn new(title: impl IntoAtoms<'a>) -> Self {
103 let title: Atoms<'_> = title.into_atoms();
104 let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
105 Self {
106 title,
107 open: None,
108 area,
109 frame: None,
110 title_frame: None,
111 resize: Resize::default()
112 .with_stroke(false)
113 .min_size([96.0, 32.0])
114 .default_size([340.0, 420.0]), scroll: ScrollArea::neither().auto_shrink(false).content_margin(0.0),
116 collapsible: true,
117 default_open: true,
118 with_title_bar: true,
119 fade_out: true,
120 auto_sized: false,
121 drag_area: WindowDrag::default(),
122 }
123 }
124
125 pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
127 let ViewportBuilder {
128 title,
129 app_id,
130 inner_size,
131 min_inner_size,
132 max_inner_size,
133 resizable,
134 decorations,
135 title_shown,
136 minimize_button,
137 .. } = viewport;
139
140 let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));
141
142 if let Some(inner_size) = inner_size {
143 window = window.default_size(inner_size);
144 }
145 if let Some(min_inner_size) = min_inner_size {
146 window = window.min_size(min_inner_size);
147 }
148 if let Some(max_inner_size) = max_inner_size {
149 window = window.max_size(max_inner_size);
150 }
151 if let Some(resizable) = resizable {
152 window = window.resizable(resizable);
153 }
154 window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
155 window = window.collapsible(minimize_button.unwrap_or(true));
156
157 window
158 }
159
160 #[inline]
162 pub fn id(mut self, id: Id) -> Self {
163 self.area = self.area.id(id);
164 self
165 }
166
167 #[inline]
173 pub fn open(mut self, open: &'a mut bool) -> Self {
174 self.open = Some(open);
175 self
176 }
177
178 #[inline]
180 pub fn enabled(mut self, enabled: bool) -> Self {
181 self.area = self.area.enabled(enabled);
182 self
183 }
184
185 #[inline]
191 pub fn interactable(mut self, interactable: bool) -> Self {
192 self.area = self.area.interactable(interactable);
193 self
194 }
195
196 #[inline]
201 pub fn movable(mut self, movable: bool) -> Self {
202 self.area = self.area.movable(movable);
203 self
204 }
205
206 #[inline]
215 pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
216 self.drag_area = drag_area;
217 self
218 }
219
220 #[inline]
222 pub fn order(mut self, order: Order) -> Self {
223 self.area = self.area.order(order);
224 self
225 }
226
227 #[inline]
231 pub fn fade_in(mut self, fade_in: bool) -> Self {
232 self.area = self.area.fade_in(fade_in);
233 self
234 }
235
236 #[inline]
242 pub fn fade_out(mut self, fade_out: bool) -> Self {
243 self.fade_out = fade_out;
244 self
245 }
246
247 #[inline]
250 pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
251 mutate(&mut self);
252 self
253 }
254
255 #[inline]
258 pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
259 self.resize = mutate(self.resize);
260 self
261 }
262
263 #[inline]
265 pub fn frame(mut self, frame: Frame) -> Self {
266 self.frame = Some(frame);
267 self
268 }
269
270 #[inline]
272 pub fn title_frame(mut self, frame: Frame) -> Self {
273 self.title_frame = Some(frame);
274 self
275 }
276
277 #[inline]
279 pub fn min_width(mut self, min_width: f32) -> Self {
280 self.resize = self.resize.min_width(min_width);
281 self
282 }
283
284 #[inline]
286 pub fn min_height(mut self, min_height: f32) -> Self {
287 self.resize = self.resize.min_height(min_height);
288 self
289 }
290
291 #[inline]
296 pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
297 self.resize = self.resize.min_size(min_size);
298 self
299 }
300
301 #[inline]
303 pub fn max_width(mut self, max_width: f32) -> Self {
304 self.resize = self.resize.max_width(max_width);
305 self
306 }
307
308 #[inline]
310 pub fn max_height(mut self, max_height: f32) -> Self {
311 self.resize = self.resize.max_height(max_height);
312 self
313 }
314
315 #[inline]
320 pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
321 self.resize = self.resize.max_size(max_size);
322 self
323 }
324
325 #[inline]
328 pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
329 self.area = self.area.current_pos(current_pos);
330 self
331 }
332
333 #[inline]
335 pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
336 self.area = self.area.default_pos(default_pos);
337 self
338 }
339
340 #[inline]
342 pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
343 self.area = self.area.fixed_pos(pos);
344 self
345 }
346
347 #[inline]
353 pub fn constrain(mut self, constrain: bool) -> Self {
354 self.area = self.area.constrain(constrain);
355 self
356 }
357
358 #[inline]
362 pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
363 self.area = self.area.constrain_to(constrain_rect);
364 self
365 }
366
367 #[inline]
375 pub fn pivot(mut self, pivot: Align2) -> Self {
376 self.area = self.area.pivot(pivot);
377 self
378 }
379
380 #[inline]
392 pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
393 self.area = self.area.anchor(align, offset);
394 self
395 }
396
397 #[inline]
399 pub fn default_open(mut self, default_open: bool) -> Self {
400 self.default_open = default_open;
401 self
402 }
403
404 #[inline]
409 pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
410 let default_size: Vec2 = default_size.into();
411 self.resize = self.resize.default_size(default_size);
412 self.area = self.area.default_size(default_size);
413 self
414 }
415
416 #[inline]
418 pub fn default_width(mut self, default_width: f32) -> Self {
419 self.resize = self.resize.default_width(default_width);
420 self.area = self.area.default_width(default_width);
421 self
422 }
423
424 #[inline]
426 pub fn default_height(mut self, default_height: f32) -> Self {
427 self.resize = self.resize.default_height(default_height);
428 self.area = self.area.default_height(default_height);
429 self
430 }
431
432 #[inline]
437 pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
438 self.resize = self.resize.fixed_size(size);
439 self
440 }
441
442 pub fn default_rect(self, rect: Rect) -> Self {
444 self.default_pos(rect.min).default_size(rect.size())
445 }
446
447 pub fn fixed_rect(self, rect: Rect) -> Self {
449 self.fixed_pos(rect.min).fixed_size(rect.size())
450 }
451
452 #[inline]
462 pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
463 let resizable = resizable.into();
464 self.resize = self.resize.resizable(resizable);
465 self
466 }
467
468 #[inline]
470 pub fn collapsible(mut self, collapsible: bool) -> Self {
471 self.collapsible = collapsible;
472 self
473 }
474
475 #[inline]
478 pub fn title_bar(mut self, title_bar: bool) -> Self {
479 self.with_title_bar = title_bar;
480 self
481 }
482
483 #[inline]
487 pub fn auto_sized(mut self) -> Self {
488 self.resize = self.resize.auto_sized();
489 self.scroll = ScrollArea::neither();
490 self.auto_sized = true;
491 self
492 }
493
494 #[inline]
498 pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
499 self.scroll = self.scroll.scroll(scroll);
500 self
501 }
502
503 #[inline]
505 pub fn hscroll(mut self, hscroll: bool) -> Self {
506 self.scroll = self.scroll.hscroll(hscroll);
507 self
508 }
509
510 #[inline]
512 pub fn vscroll(mut self, vscroll: bool) -> Self {
513 self.scroll = self.scroll.vscroll(vscroll);
514 self
515 }
516
517 #[inline]
523 pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
524 self.scroll = self.scroll.scroll_source(ScrollSource {
525 drag: drag_to_scroll,
526 ..Default::default()
527 });
528 self
529 }
530
531 #[inline]
533 pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
534 self.scroll = self.scroll.scroll_bar_visibility(visibility);
535 self
536 }
537}
538
539impl Window<'_> {
540 #[inline]
543 pub fn show<R>(
544 self,
545 ctx: &Context,
546 add_contents: impl FnOnce(&mut Ui) -> R,
547 ) -> Option<InnerResponse<Option<R>>> {
548 self.show_dyn(ctx, Box::new(add_contents))
549 }
550
551 fn show_dyn<'c, R>(
552 self,
553 ctx: &Context,
554 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
555 ) -> Option<InnerResponse<Option<R>>> {
556 let Window {
557 title,
558 mut open,
559 area,
560 frame,
561 title_frame,
562 resize,
563 scroll,
564 collapsible,
565 default_open,
566 with_title_bar,
567 fade_out,
568 auto_sized,
569 drag_area: drag_area_setting,
570 } = self;
571
572 let effective_drag = if !area.is_movable() || drag_area_setting == WindowDrag::Off {
577 WindowDrag::Off
578 } else if !with_title_bar {
579 WindowDrag::Anywhere
580 } else {
581 drag_area_setting.resolve(ctx)
582 };
583
584 let area = if effective_drag == WindowDrag::Off {
590 area.movable(false)
591 } else {
592 area
593 };
594
595 let title_drag_mode = effective_drag == WindowDrag::TitleBar;
607 let pivot_pos_before_begin = if title_drag_mode {
608 if let Some(resp) = ctx.read_response(area.id.with("__title_click"))
609 && resp.dragged()
610 {
611 let delta = ctx.input(|i| i.pointer.delta());
612 if delta != Vec2::ZERO {
613 ctx.memory_mut(|mem| {
614 if let Some(state) = mem.areas_mut().get_mut(area.id)
615 && let Some(pivot_pos) = state.pivot_pos.as_mut()
616 {
617 *pivot_pos += delta;
618 }
619 });
620 }
621 }
622 area::AreaState::load(ctx, area.id).and_then(|s| s.pivot_pos)
623 } else {
624 None
625 };
626
627 let style = ctx.global_style();
628
629 let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
631 let window_title_frame = title_frame.unwrap_or(window_frame);
632
633 let window_content_margin = window_frame.inner_margin;
635 let window_frame = window_frame.inner_margin(0.0);
636
637 let is_explicitly_closed = matches!(open, Some(false));
638 let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
639 let opacity = ctx.animate_bool_with_easing(
640 area.id.with("fade-out"),
641 is_open,
642 emath::easing::cubic_out,
643 );
644 if opacity <= 0.0 {
645 return None;
646 }
647
648 let area_id = area.id;
649 let area_layer_id = area.layer();
650 let resize_id = area_id.with("resize");
651 let mut collapsing =
652 CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
653
654 let is_collapsed = with_title_bar && !collapsing.is_open();
655 let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
656
657 let resize = resize.resizable(false); let mut resize = resize.id(resize_id);
659
660 let on_top = Some(area_layer_id) == ctx.top_layer_id();
661 let mut area = area.begin(ctx);
662
663 if let Some(pre_begin_pivot) = pivot_pos_before_begin {
669 let constrain = area.constrain();
670 let constrain_rect = area.constrain_rect();
671 let state = area.state_mut();
672 state.pivot_pos = Some(pre_begin_pivot);
673 if constrain {
674 state.set_left_top_pos(
675 Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
676 );
677 }
678 state.set_left_top_pos(area::round_area_position(ctx, state.left_top_pos()));
679 }
680
681 area.with_widget_info(|| {
682 WidgetInfo::labeled(
683 WidgetType::Window,
684 true,
685 title.text().as_deref().unwrap_or(""),
686 )
687 });
688
689 {
690 let constrain_rect = area.constrain_rect();
693 let max_width = constrain_rect.width();
694 let max_height = constrain_rect.height();
695 resize.max_size.x = resize.max_size.x.min(max_width);
696 resize.max_size.y = resize.max_size.y.min(max_height);
697 }
698
699 {
704 let frame_margin = window_frame.total_margin().sum();
705 resize.min_size = (resize.min_size - frame_margin).at_least(Vec2::ZERO);
706 resize.max_size = (resize.max_size - frame_margin).at_least(Vec2::ZERO);
707 resize.default_size = (resize.default_size - frame_margin).at_least(Vec2::ZERO);
708 }
709
710 let mut area_content_ui = area.content_ui(ctx);
711
712 if is_open {
713 } else if fade_out {
716 area_content_ui.multiply_opacity(opacity);
717 }
718
719 let content_inner = {
720 let outer_response = window_frame.show(&mut area_content_ui, |ui| {
721 resize.show(ui, |ui| {
722 if with_title_bar {
723 title_ui(
724 ui,
725 title,
726 window_title_frame,
727 &mut collapsing,
728 collapsible,
729 on_top,
730 open.as_deref_mut(),
731 auto_sized,
732 effective_drag == WindowDrag::TitleBar,
733 area_id,
734 );
735 }
736 collapsing
737 .show_body_unindented(ui, |ui| {
738 if scroll.is_any_scroll_enabled() {
739 scroll
740 .content_margin(window_content_margin)
741 .show(ui, add_contents)
742 .inner
743 } else {
744 crate::Frame::NONE
745 .inner_margin(window_content_margin)
746 .show(ui, add_contents)
747 .inner
748 }
749 })
750 .map(|inner| inner.inner)
751 })
752 });
753
754 let outer_rect = outer_response.response.rect;
755
756 let resize_interaction = do_resize_interaction(
758 ctx,
759 possible,
760 area.id(),
761 area_layer_id,
762 outer_rect,
763 window_frame,
764 );
765
766 paint_resize_corner(
767 &area_content_ui,
768 &possible,
769 outer_rect,
770 &window_frame,
771 resize_interaction,
772 );
773
774 {
775 let margins = window_frame.total_margin().sum();
776
777 resize_response(
778 resize_interaction,
779 ctx,
780 margins,
781 area_layer_id,
782 &mut area,
783 resize_id,
784 );
785 }
786 collapsing.store(ctx);
789
790 paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
791
792 outer_response.inner
793 };
794
795 let full_response = area.end(ctx, area_content_ui);
796
797 if full_response.should_close()
798 && let Some(open) = open
799 {
800 *open = false;
801 }
802
803 let inner_response = InnerResponse {
804 inner: content_inner,
805 response: full_response,
806 };
807 Some(inner_response)
808 }
809}
810
811fn paint_resize_corner(
812 ui: &Ui,
813 possible: &PossibleInteractions,
814 outer_rect: Rect,
815 window_frame: &Frame,
816 i: ResizeInteraction,
817) {
818 let cr = window_frame.corner_radius;
819
820 let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
821 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
822 } else if possible.resize_left && possible.resize_bottom {
823 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
824 } else if possible.resize_left && possible.resize_top {
825 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
826 } else if possible.resize_right && possible.resize_top {
827 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
828 } else {
829 if possible.resize_right || possible.resize_bottom {
833 (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
834 } else if possible.resize_left || possible.resize_bottom {
835 (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
836 } else if possible.resize_left || possible.resize_top {
837 (Align2::LEFT_TOP, cr.nw, i.left & i.top)
838 } else if possible.resize_right || possible.resize_top {
839 (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
840 } else {
841 return;
842 }
843 };
844
845 let radius = radius as f32;
847 let offset =
848 ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
849
850 let stroke = if corner_response.drag {
851 ui.visuals().widgets.active.fg_stroke
852 } else if corner_response.hover {
853 ui.visuals().widgets.hovered.fg_stroke
854 } else {
855 window_frame.stroke
856 };
857
858 let fill_rect = outer_rect.shrink(window_frame.stroke.width);
859 let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
860 let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
861 let corner_rect = corner_rect.translate(-offset * corner.to_sign()); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
863}
864
865#[derive(Clone, Copy, Debug)]
869struct PossibleInteractions {
870 resize_left: bool,
872 resize_right: bool,
873 resize_top: bool,
874 resize_bottom: bool,
875}
876
877impl PossibleInteractions {
878 fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
879 let movable = area.is_enabled() && area.is_movable();
880 let resizable = resize
881 .is_resizable()
882 .and(area.is_enabled() && !is_collapsed);
883 let pivot = area.get_pivot();
884 Self {
885 resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
886 resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
887 resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
888 resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
889 }
890 }
891
892 pub fn resizable(&self) -> bool {
893 self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
894 }
895}
896
897#[derive(Clone, Copy, Debug)]
899struct ResizeInteraction {
900 outer_rect: Rect,
902
903 window_frame: Frame,
904
905 left: SideResponse,
906 right: SideResponse,
907 top: SideResponse,
908 bottom: SideResponse,
909}
910
911#[derive(Clone, Copy, Debug, Default)]
913struct SideResponse {
914 hover: bool,
915 drag: bool,
916}
917
918impl SideResponse {
919 pub fn any(&self) -> bool {
920 self.hover || self.drag
921 }
922}
923
924impl core::ops::BitAnd for SideResponse {
925 type Output = Self;
926
927 fn bitand(self, rhs: Self) -> Self::Output {
928 Self {
929 hover: self.hover && rhs.hover,
930 drag: self.drag && rhs.drag,
931 }
932 }
933}
934
935impl core::ops::BitOrAssign for SideResponse {
936 fn bitor_assign(&mut self, rhs: Self) {
937 *self = Self {
938 hover: self.hover || rhs.hover,
939 drag: self.drag || rhs.drag,
940 };
941 }
942}
943
944impl ResizeInteraction {
945 pub fn set_cursor(&self, ctx: &Context) {
946 let left = self.left.any();
947 let right = self.right.any();
948 let top = self.top.any();
949 let bottom = self.bottom.any();
950
951 if (left && top) || (right && bottom) {
953 ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
954 } else if (right && top) || (left && bottom) {
955 ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
956 } else if left || right {
957 ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
958 } else if bottom || top {
959 ctx.set_cursor_icon(CursorIcon::ResizeVertical);
960 }
961 }
962
963 pub fn any_hovered(&self) -> bool {
964 self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
965 }
966
967 pub fn any_dragged(&self) -> bool {
968 self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
969 }
970}
971
972fn resize_response(
973 resize_interaction: ResizeInteraction,
974 ctx: &Context,
975 margins: Vec2,
976 area_layer_id: LayerId,
977 area: &mut area::Prepared,
978 resize_id: Id,
979) {
980 let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
981 return;
982 };
983
984 if area.constrain() {
985 new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
986 }
987
988 area.state_mut().set_left_top_pos(new_rect.left_top());
990
991 if resize_interaction.any_dragged()
992 && let Some(mut state) = resize::State::load(ctx, resize_id)
993 {
994 state.requested_size = Some(new_rect.size() - margins);
995 state.store(ctx, resize_id);
996 }
997
998 ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
999}
1000
1001fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
1003 let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
1005
1006 if !interaction.any_dragged() {
1007 ctx.data_mut(|data| {
1008 data.remove::<Rect>(rect_at_start_of_drag_id);
1009 });
1010 return None;
1011 }
1012
1013 let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
1014
1015 let rect_at_start_of_drag = ctx.data_mut(|data| {
1016 *data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
1017 });
1018
1019 let mut rect = rect_at_start_of_drag; rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1023
1024 if interaction.left.drag {
1025 rect.min.x += total_drag_delta.x;
1026 } else if interaction.right.drag {
1027 rect.max.x += total_drag_delta.x;
1028 }
1029
1030 if interaction.top.drag {
1031 rect.min.y += total_drag_delta.y;
1032 } else if interaction.bottom.drag {
1033 rect.max.y += total_drag_delta.y;
1034 }
1035
1036 rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
1038
1039 Some(rect.round_ui())
1040}
1041
1042fn do_resize_interaction(
1043 ctx: &Context,
1044 possible: PossibleInteractions,
1045 accessibility_parent: Id,
1046 layer_id: LayerId,
1047 outer_rect: Rect,
1048 window_frame: Frame,
1049) -> ResizeInteraction {
1050 if !possible.resizable() {
1051 return ResizeInteraction {
1052 outer_rect,
1053 window_frame,
1054 left: Default::default(),
1055 right: Default::default(),
1056 top: Default::default(),
1057 bottom: Default::default(),
1058 };
1059 }
1060
1061 let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
1063
1064 let side_response = |rect, id| {
1065 ctx.register_accesskit_parent(id, accessibility_parent);
1066 let response = ctx.create_widget(
1067 WidgetRect {
1068 layer_id,
1069 id,
1070 parent_id: layer_id.id,
1071 rect,
1072 interact_rect: rect,
1073 sense: Sense::DRAG, enabled: true,
1075 },
1076 true,
1077 InteractOptions {
1078 move_to_top: true,
1082 },
1083 );
1084
1085 response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle));
1086
1087 SideResponse {
1088 hover: response.hovered(),
1089 drag: response.dragged(),
1090 }
1091 };
1092
1093 let id = Id::new(layer_id).with("edge_drag");
1094
1095 let style = ctx.global_style();
1096
1097 let side_grab_radius = style.interaction.resize_grab_radius_side;
1098 let corner_grab_radius = style.interaction.resize_grab_radius_corner;
1099
1100 let vertical_rect = |a: Pos2, b: Pos2| {
1101 Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
1102 };
1103 let horizontal_rect = |a: Pos2, b: Pos2| {
1104 Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
1105 };
1106 let corner_rect =
1107 |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
1108
1109 let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
1111
1112 if possible.resize_right {
1116 let response = side_response(
1117 vertical_rect(rect.right_top(), rect.right_bottom()),
1118 id.with("right"),
1119 );
1120 right |= response;
1121 }
1122 if possible.resize_left {
1123 let response = side_response(
1124 vertical_rect(rect.left_top(), rect.left_bottom()),
1125 id.with("left"),
1126 );
1127 left |= response;
1128 }
1129 if possible.resize_bottom {
1130 let response = side_response(
1131 horizontal_rect(rect.left_bottom(), rect.right_bottom()),
1132 id.with("bottom"),
1133 );
1134 bottom |= response;
1135 }
1136 if possible.resize_top {
1137 let response = side_response(
1138 horizontal_rect(rect.left_top(), rect.right_top()),
1139 id.with("top"),
1140 );
1141 top |= response;
1142 }
1143
1144 if possible.resize_right || possible.resize_bottom {
1153 let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
1154 if possible.resize_right {
1155 right |= response;
1156 }
1157 if possible.resize_bottom {
1158 bottom |= response;
1159 }
1160 }
1161
1162 if possible.resize_right || possible.resize_top {
1163 let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
1164 if possible.resize_right {
1165 right |= response;
1166 }
1167 if possible.resize_top {
1168 top |= response;
1169 }
1170 }
1171
1172 if possible.resize_left || possible.resize_bottom {
1173 let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
1174 if possible.resize_left {
1175 left |= response;
1176 }
1177 if possible.resize_bottom {
1178 bottom |= response;
1179 }
1180 }
1181
1182 if possible.resize_left || possible.resize_top {
1183 let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
1184 if possible.resize_left {
1185 left |= response;
1186 }
1187 if possible.resize_top {
1188 top |= response;
1189 }
1190 }
1191
1192 let interaction = ResizeInteraction {
1193 outer_rect,
1194 window_frame,
1195 left,
1196 right,
1197 top,
1198 bottom,
1199 };
1200 interaction.set_cursor(ctx);
1201 interaction
1202}
1203
1204fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
1206 use epaint::tessellator::path::add_circle_quadrant;
1207
1208 let visuals = if interaction.any_dragged() {
1209 ui.style().visuals.widgets.active
1210 } else if interaction.any_hovered() {
1211 ui.style().visuals.widgets.hovered
1212 } else {
1213 return;
1214 };
1215
1216 let [left, right, top, bottom]: [bool; 4];
1217
1218 if interaction.any_dragged() {
1219 left = interaction.left.drag;
1220 right = interaction.right.drag;
1221 top = interaction.top.drag;
1222 bottom = interaction.bottom.drag;
1223 } else {
1224 left = interaction.left.hover;
1225 right = interaction.right.hover;
1226 top = interaction.top.hover;
1227 bottom = interaction.bottom.hover;
1228 }
1229
1230 let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
1231
1232 let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1234
1235 let stroke = visuals.bg_stroke;
1237 let half_stroke = stroke.width / 2.0;
1238 let rect = rect
1239 .shrink(half_stroke)
1240 .round_to_pixels(ui.pixels_per_point())
1241 .expand(half_stroke);
1242
1243 let Rect { min, max } = rect;
1244
1245 let mut points = Vec::new();
1246
1247 if right && !bottom && !top {
1248 points.push(pos2(max.x, min.y + cr.ne));
1249 points.push(pos2(max.x, max.y - cr.se));
1250 }
1251 if right && bottom {
1252 points.push(pos2(max.x, min.y + cr.ne));
1253 points.push(pos2(max.x, max.y - cr.se));
1254 add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
1255 }
1256 if bottom {
1257 points.push(pos2(max.x - cr.se, max.y));
1258 points.push(pos2(min.x + cr.sw, max.y));
1259 }
1260 if left && bottom {
1261 add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
1262 }
1263 if left {
1264 points.push(pos2(min.x, max.y - cr.sw));
1265 points.push(pos2(min.x, min.y + cr.nw));
1266 }
1267 if left && top {
1268 add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
1269 }
1270 if top {
1271 points.push(pos2(min.x + cr.nw, min.y));
1272 points.push(pos2(max.x - cr.ne, min.y));
1273 }
1274 if right && top {
1275 add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
1276 points.push(pos2(max.x, min.y + cr.ne));
1277 points.push(pos2(max.x, max.y - cr.se));
1278 }
1279
1280 ui.painter().add(Shape::line(points, stroke));
1281}
1282
1283#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
1291fn title_ui(
1292 ui: &mut Ui,
1293 mut title: Atoms<'_>,
1294 frame: Frame,
1295 collapsing: &mut CollapsingState,
1296 collapsible: bool,
1297 active: bool,
1298 open: Option<&mut bool>,
1299 auto_sized: bool,
1300 drag_to_move: bool,
1301 area_id: Id,
1302) -> Response {
1303 let shape_idx = ui.painter().add(Shape::Noop);
1304
1305 let mut atoms = Atoms::default();
1306
1307 let button_size = Vec2::splat(ui.spacing().icon_width);
1308
1309 let heading_font_height =
1313 ui.fonts_mut(|f| f.row_height(&TextStyle::Heading.resolve(ui.style())));
1314 let button_allocation_size = Vec2::splat(heading_font_height);
1315 let button_shrink = (button_allocation_size - button_size) / 2.0;
1316
1317 let collapse_atom_id = Id::new("__window_collapse_button");
1318 let close_atom_id = Id::new("__window_close_button");
1319
1320 let expanded = collapsing.openness(ui.ctx()) > 0.0;
1321
1322 if collapsible {
1323 atoms.push_right(Atom::custom(collapse_atom_id, button_allocation_size));
1324 }
1325
1326 atoms.push_right(Atom::grow());
1327
1328 if !auto_sized
1329 && !title.any_shrink()
1330 && let Some(first_text) = title
1331 .iter_mut()
1332 .find(|a| matches!(a.kind, AtomKind::Text(..)))
1333 {
1334 first_text.shrink = true;
1335 }
1336 atoms.extend_right(title);
1337
1338 atoms.push_right(Atom::grow());
1339
1340 if open.is_some() {
1341 atoms.push_right(Atom::custom(close_atom_id, button_allocation_size));
1342 }
1343
1344 let spacing = ui.spacing().item_spacing.x;
1345
1346 let mut child_ui = ui.new_child(UiBuilder::new());
1347
1348 let mut layout = AtomLayout::new(atoms)
1349 .gap(spacing)
1350 .fallback_font(TextStyle::Heading)
1351 .wrap_mode(TextWrapMode::Truncate)
1352 .frame(Frame::NONE.inner_margin(frame.inner_margin));
1353
1354 let frame = frame.inner_margin(0); if expanded {
1357 let min_width = if auto_sized {
1358 ui.response().rect.width()
1362 } else {
1363 child_ui.available_width()
1364 };
1365
1366 layout = layout.min_size(Vec2::new(min_width, 0.0));
1367 }
1368
1369 let layout_response = layout.show(&mut child_ui);
1370
1371 let mut title_click_rect = layout_response.response.rect + frame.total_margin();
1372
1373 if collapsible && let Some(rect) = layout_response.rect(collapse_atom_id) {
1375 let rect = rect.shrink2(button_shrink);
1376 title_click_rect = title_click_rect.with_min_x(rect.max.x);
1377 let icon_response = child_ui.interact(
1378 rect,
1379 child_ui.auto_id_with("collapse_button"),
1380 Sense::click(),
1381 );
1382 icon_response.widget_info(|| {
1383 WidgetInfo::labeled(
1384 WidgetType::Button,
1385 child_ui.is_enabled(),
1386 if collapsing.is_open() { "Hide" } else { "Show" },
1387 )
1388 });
1389 if icon_response.clicked() {
1390 collapsing.toggle(&child_ui);
1391 }
1392 let openness = collapsing.openness(child_ui.ctx());
1393 crate::collapsing_header::paint_default_icon(&mut child_ui, openness, &icon_response);
1394 }
1395
1396 if let Some(open) = open
1398 && let Some(rect) = layout_response.rect(close_atom_id)
1399 {
1400 let rect = rect.shrink2(button_shrink);
1401 title_click_rect = title_click_rect.with_max_x(rect.min.x);
1402 if close_button(&mut child_ui, rect).clicked() {
1403 *open = false;
1404 }
1405 }
1406
1407 if collapsible || drag_to_move {
1408 let sense = if drag_to_move {
1413 Sense::click_and_drag()
1414 } else {
1415 Sense::click()
1416 };
1417 let response = child_ui.interact(title_click_rect, area_id.with("__title_click"), sense);
1418
1419 if collapsible && response.double_clicked() {
1420 collapsing.toggle(&child_ui);
1421 }
1422 }
1423
1424 {
1425 let mut header_frame = frame.shadow(Shadow::NONE);
1426 if active {
1427 header_frame = header_frame.fill(ui.visuals().widgets.open.weak_bg_fill);
1428 }
1429 if expanded {
1430 header_frame.corner_radius.sw = 0;
1431 header_frame.corner_radius.se = 0;
1432 }
1433 ui.painter()
1434 .set(shape_idx, header_frame.paint(layout_response.rect));
1435 }
1436
1437 let mut advance_rect = child_ui.min_rect();
1438
1439 if auto_sized {
1440 advance_rect = advance_rect.with_max_x(advance_rect.min.x);
1443 }
1444 if expanded {
1445 advance_rect.max.y += frame.total_margin().bottom + frame.inner_margin.top as f32
1448 - child_ui.spacing().item_spacing.y;
1449 }
1450
1451 ui.advance_cursor_after_rect(advance_rect);
1452
1453 layout_response.response
1454}
1455
1456fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1467 let close_id = ui.auto_id_with("window_close_button");
1468 let response = ui.interact(rect, close_id, Sense::click());
1469 response
1470 .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1471
1472 ui.expand_to_include_rect(response.rect);
1473
1474 let visuals = ui.style().interact(&response);
1475 let rect = rect.shrink(2.0).expand(visuals.expansion);
1476 let stroke = visuals.fg_stroke;
1477 ui.painter() .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1479 ui.painter() .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1481 response
1482}