egui/containers/
window.rs

1// WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts.
2
3use 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;
12use super::{area, resize, Area, Frame, Resize, ScrollArea};
13
14/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
15///
16/// You can customize:
17/// * title
18/// * default, minimum, maximum and/or fixed size, collapsed/expanded
19/// * if the window has a scroll area (off by default)
20/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
21/// * if there should be a close button (none by default)
22///
23/// ```
24/// # egui::__run_test_ctx(|ctx| {
25/// egui::Window::new("My Window").show(ctx, |ui| {
26///    ui.label("Hello World!");
27/// });
28/// # });
29/// ```
30///
31/// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`].
32///
33/// Note that this is NOT a native OS window.
34/// To create a new native OS window, use [`crate::Context::show_viewport_deferred`].
35#[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    /// The window title is used as a unique [`Id`] and must be unique, and should not change.
51    /// This is true even if you disable the title bar with `.title_bar(false)`.
52    /// If you need a changing title, you must call `window.id(…)` with a fixed id.
53    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]), // Default inner size of a window
65            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    /// Assign a unique id to the Window. Required if the title changes, or is shared with another window.
74    #[inline]
75    pub fn id(mut self, id: Id) -> Self {
76        self.area = self.area.id(id);
77        self
78    }
79
80    /// Call this to add a close-button to the window title bar.
81    ///
82    /// * If `*open == false`, the window will not be visible.
83    /// * If `*open == true`, the window will have a close button.
84    /// * If the close button is pressed, `*open` will be set to `false`.
85    #[inline]
86    pub fn open(mut self, open: &'open mut bool) -> Self {
87        self.open = Some(open);
88        self
89    }
90
91    /// If `false` the window will be grayed out and non-interactive.
92    #[inline]
93    pub fn enabled(mut self, enabled: bool) -> Self {
94        self.area = self.area.enabled(enabled);
95        self
96    }
97
98    /// If false, clicks goes straight through to what is behind us.
99    ///
100    /// Can be used for semi-invisible areas that the user should be able to click through.
101    ///
102    /// Default: `true`.
103    #[inline]
104    pub fn interactable(mut self, interactable: bool) -> Self {
105        self.area = self.area.interactable(interactable);
106        self
107    }
108
109    /// If `false` the window will be immovable.
110    #[inline]
111    pub fn movable(mut self, movable: bool) -> Self {
112        self.area = self.area.movable(movable);
113        self
114    }
115
116    /// `order(Order::Foreground)` for a Window that should always be on top
117    #[inline]
118    pub fn order(mut self, order: Order) -> Self {
119        self.area = self.area.order(order);
120        self
121    }
122
123    /// If `true`, quickly fade in the `Window` when it first appears.
124    ///
125    /// Default: `true`.
126    #[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    /// If `true`, quickly fade out the `Window` when it closes.
133    ///
134    /// This only works if you use [`Self::open`] to close the window.
135    ///
136    /// Default: `true`.
137    #[inline]
138    pub fn fade_out(mut self, fade_out: bool) -> Self {
139        self.fade_out = fade_out;
140        self
141    }
142
143    /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
144    // TODO(emilk): I'm not sure this is a good interface for this.
145    #[inline]
146    pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
147        mutate(&mut self);
148        self
149    }
150
151    /// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
152    // TODO(emilk): I'm not sure this is a good interface for this.
153    #[inline]
154    pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
155        self.resize = mutate(self.resize);
156        self
157    }
158
159    /// Change the background color, margins, etc.
160    #[inline]
161    pub fn frame(mut self, frame: Frame) -> Self {
162        self.frame = Some(frame);
163        self
164    }
165
166    /// Set minimum width of the window.
167    #[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    /// Set minimum height of the window.
174    #[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    /// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`.
181    #[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    /// Set maximum width of the window.
188    #[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    /// Set maximum height of the window.
195    #[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    /// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`.
202    #[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    /// Set current position of the window.
209    /// If the window is movable it is up to you to keep track of where it moved to!
210    #[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    /// Set initial position of the window.
217    #[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    /// Sets the window position and prevents it from being dragged around.
224    #[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    /// Constrains this window to [`Context::screen_rect`].
231    ///
232    /// To change the area to constrain to, use [`Self::constrain_to`].
233    ///
234    /// Default: `true`.
235    #[inline]
236    pub fn constrain(mut self, constrain: bool) -> Self {
237        self.area = self.area.constrain(constrain);
238        self
239    }
240
241    /// Constrain the movement of the window to the given rectangle.
242    ///
243    /// For instance: `.constrain_to(ctx.screen_rect())`.
244    #[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    /// Where the "root" of the window is.
251    ///
252    /// For instance, if you set this to [`Align2::RIGHT_TOP`]
253    /// then [`Self::fixed_pos`] will set the position of the right-top
254    /// corner of the window.
255    ///
256    /// Default: [`Align2::LEFT_TOP`].
257    #[inline]
258    pub fn pivot(mut self, pivot: Align2) -> Self {
259        self.area = self.area.pivot(pivot);
260        self
261    }
262
263    /// Set anchor and distance.
264    ///
265    /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
266    /// in the right-top corner of the screen".
267    ///
268    /// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
269    /// would move the window left and down from the given anchor.
270    ///
271    /// Anchoring also makes the window immovable.
272    ///
273    /// It is an error to set both an anchor and a position.
274    #[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    /// Set initial collapsed state of the window
281    #[inline]
282    pub fn default_open(mut self, default_open: bool) -> Self {
283        self.default_open = default_open;
284        self
285    }
286
287    /// Set initial size of the window.
288    #[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    /// Set initial width of the window.
297    #[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    /// Set initial height of the window.
305    #[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    /// Sets the window size and prevents it from being resized by dragging its edges.
313    #[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    /// Set initial position and size of the window.
320    pub fn default_rect(self, rect: Rect) -> Self {
321        self.default_pos(rect.min).default_size(rect.size())
322    }
323
324    /// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
325    pub fn fixed_rect(self, rect: Rect) -> Self {
326        self.fixed_pos(rect.min).fixed_size(rect.size())
327    }
328
329    /// Can the user resize the window by dragging its edges?
330    ///
331    /// Note that even if you set this to `false` the window may still auto-resize.
332    ///
333    /// You can set the window to only be resizable in one direction by using
334    /// e.g. `[true, false]` as the argument,
335    /// making the window only resizable in the x-direction.
336    ///
337    /// Default is `true`.
338    #[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    /// Can the window be collapsed by clicking on its title?
346    #[inline]
347    pub fn collapsible(mut self, collapsible: bool) -> Self {
348        self.collapsible = collapsible;
349        self
350    }
351
352    /// Show title bar on top of the window?
353    /// If `false`, the window will not be collapsible nor have a close-button.
354    #[inline]
355    pub fn title_bar(mut self, title_bar: bool) -> Self {
356        self.with_title_bar = title_bar;
357        self
358    }
359
360    /// Not resizable, just takes the size of its contents.
361    /// Also disabled scrolling.
362    /// Text will not wrap, but will instead make your window width expand.
363    #[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    /// Enable/disable horizontal/vertical scrolling. `false` by default.
371    ///
372    /// You can pass in `false`, `true`, `[false, true]` etc.
373    #[inline]
374    pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
375        self.scroll = self.scroll.scroll(scroll);
376        self
377    }
378
379    /// Enable/disable horizontal/vertical scrolling. `false` by default.
380    #[deprecated = "Renamed to `scroll`"]
381    #[inline]
382    pub fn scroll2(mut self, scroll: impl Into<Vec2b>) -> Self {
383        self.scroll = self.scroll.scroll(scroll);
384        self
385    }
386
387    /// Enable/disable horizontal scrolling. `false` by default.
388    #[inline]
389    pub fn hscroll(mut self, hscroll: bool) -> Self {
390        self.scroll = self.scroll.hscroll(hscroll);
391        self
392    }
393
394    /// Enable/disable vertical scrolling. `false` by default.
395    #[inline]
396    pub fn vscroll(mut self, vscroll: bool) -> Self {
397        self.scroll = self.scroll.vscroll(vscroll);
398        self
399    }
400
401    /// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
402    ///
403    /// See [`ScrollArea::drag_to_scroll`] for more.
404    #[inline]
405    pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
406        self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
407        self
408    }
409
410    /// Sets the [`ScrollBarVisibility`] of the window.
411    #[inline]
412    pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
413        self.scroll = self.scroll.scroll_bar_visibility(visibility);
414        self
415    }
416}
417
418impl Window<'_> {
419    /// Returns `None` if the window is not open (if [`Window::open`] was called with `&mut false`).
420    /// Returns `Some(InnerResponse { inner: None })` if the window is collapsed.
421    #[inline]
422    pub fn show<R>(
423        self,
424        ctx: &Context,
425        add_contents: impl FnOnce(&mut Ui) -> R,
426    ) -> Option<InnerResponse<Option<R>>> {
427        self.show_dyn(ctx, Box::new(add_contents))
428    }
429
430    fn show_dyn<'c, R>(
431        self,
432        ctx: &Context,
433        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
434    ) -> Option<InnerResponse<Option<R>>> {
435        let Window {
436            title,
437            open,
438            area,
439            frame,
440            resize,
441            scroll,
442            collapsible,
443            default_open,
444            with_title_bar,
445            fade_out,
446        } = self;
447
448        let header_color =
449            frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
450        let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
451
452        let is_explicitly_closed = matches!(open, Some(false));
453        let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
454        let opacity = ctx.animate_bool_with_easing(
455            area.id.with("fade-out"),
456            is_open,
457            emath::easing::cubic_out,
458        );
459        if opacity <= 0.0 {
460            return None;
461        }
462
463        let area_id = area.id;
464        let area_layer_id = area.layer();
465        let resize_id = area_id.with("resize");
466        let mut collapsing =
467            CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
468
469        let is_collapsed = with_title_bar && !collapsing.is_open();
470        let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
471
472        let resize = resize.resizable(false); // We resize it manually
473        let mut resize = resize.id(resize_id);
474
475        let on_top = Some(area_layer_id) == ctx.top_layer_id();
476        let mut area = area.begin(ctx);
477
478        area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
479
480        // Calculate roughly how much larger the full window inner size is compared to the content rect
481        let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
482            let style = ctx.style();
483            let title_bar_inner_height = ctx
484                .fonts(|fonts| title.font_height(fonts, &style))
485                .at_least(style.spacing.interact_size.y);
486            let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
487            let half_height = (title_bar_inner_height / 2.0).round() as _;
488            window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height);
489            window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height);
490
491            let title_content_spacing = if is_collapsed {
492                0.0
493            } else {
494                window_frame.stroke.width
495            };
496            (title_bar_inner_height, title_content_spacing)
497        } else {
498            (0.0, 0.0)
499        };
500
501        {
502            // Prevent window from becoming larger than the constrain rect.
503            let constrain_rect = area.constrain_rect();
504            let max_width = constrain_rect.width();
505            let max_height =
506                constrain_rect.height() - title_bar_height_with_margin - title_content_spacing;
507            resize.max_size.x = resize.max_size.x.min(max_width);
508            resize.max_size.y = resize.max_size.y.min(max_height);
509        }
510
511        // First check for resize to avoid frame delay:
512        let last_frame_outer_rect = area.state().rect();
513        let resize_interaction = ctx.with_accessibility_parent(area.id(), || {
514            resize_interaction(
515                ctx,
516                possible,
517                area_layer_id,
518                last_frame_outer_rect,
519                window_frame,
520            )
521        });
522
523        {
524            let margins = window_frame.total_margin().sum()
525                + vec2(0.0, title_bar_height_with_margin + title_content_spacing);
526
527            resize_response(
528                resize_interaction,
529                ctx,
530                margins,
531                area_layer_id,
532                &mut area,
533                resize_id,
534            );
535        }
536
537        let mut area_content_ui = area.content_ui(ctx);
538        if is_open {
539            // `Area` already takes care of fade-in animations,
540            // so we only need to handle fade-out animations here.
541        } else if fade_out {
542            area_content_ui.multiply_opacity(opacity);
543        }
544
545        let content_inner = {
546            ctx.with_accessibility_parent(area.id(), || {
547                // BEGIN FRAME --------------------------------
548                let mut frame = window_frame.begin(&mut area_content_ui);
549
550                let show_close_button = open.is_some();
551
552                let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
553
554                let title_bar = if with_title_bar {
555                    let title_bar = TitleBar::new(
556                        &frame.content_ui,
557                        title,
558                        show_close_button,
559                        collapsible,
560                        window_frame,
561                        title_bar_height_with_margin,
562                    );
563                    resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width
564
565                    frame.content_ui.set_min_size(title_bar.inner_rect.size());
566
567                    // Skip the title bar (and separator):
568                    if is_collapsed {
569                        frame.content_ui.add_space(title_bar.inner_rect.height());
570                    } else {
571                        frame.content_ui.add_space(
572                            title_bar.inner_rect.height()
573                                + title_content_spacing
574                                + window_frame.inner_margin.sum().y,
575                        );
576                    }
577
578                    Some(title_bar)
579                } else {
580                    None
581                };
582
583                let (content_inner, content_response) = collapsing
584                    .show_body_unindented(&mut frame.content_ui, |ui| {
585                        resize.show(ui, |ui| {
586                            if scroll.is_any_scroll_enabled() {
587                                scroll.show(ui, add_contents).inner
588                            } else {
589                                add_contents(ui)
590                            }
591                        })
592                    })
593                    .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
594
595                let outer_rect = frame.end(&mut area_content_ui).rect;
596                paint_resize_corner(
597                    &area_content_ui,
598                    &possible,
599                    outer_rect,
600                    &window_frame,
601                    resize_interaction,
602                );
603
604                // END FRAME --------------------------------
605
606                if let Some(mut title_bar) = title_bar {
607                    title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
608                    title_bar.inner_rect.max.y =
609                        title_bar.inner_rect.min.y + title_bar_height_with_margin;
610
611                    if on_top && area_content_ui.visuals().window_highlight_topmost {
612                        let mut round =
613                            window_frame.corner_radius - window_frame.stroke.width.round() as u8;
614
615                        if !is_collapsed {
616                            round.se = 0;
617                            round.sw = 0;
618                        }
619
620                        area_content_ui.painter().set(
621                            *where_to_put_header_background,
622                            RectShape::filled(title_bar.inner_rect, round, header_color),
623                        );
624                    };
625
626                    if false {
627                        ctx.debug_painter().debug_rect(
628                            title_bar.inner_rect,
629                            Color32::LIGHT_BLUE,
630                            "title_bar.rect",
631                        );
632                    }
633
634                    title_bar.ui(
635                        &mut area_content_ui,
636                        &content_response,
637                        open,
638                        &mut collapsing,
639                        collapsible,
640                    );
641                }
642
643                collapsing.store(ctx);
644
645                paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
646
647                content_inner
648            })
649        };
650
651        let full_response = area.end(ctx, area_content_ui);
652
653        let inner_response = InnerResponse {
654            inner: content_inner,
655            response: full_response,
656        };
657        Some(inner_response)
658    }
659}
660
661fn paint_resize_corner(
662    ui: &Ui,
663    possible: &PossibleInteractions,
664    outer_rect: Rect,
665    window_frame: &Frame,
666    i: ResizeInteraction,
667) {
668    let cr = window_frame.corner_radius;
669
670    let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
671        (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
672    } else if possible.resize_left && possible.resize_bottom {
673        (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
674    } else if possible.resize_left && possible.resize_top {
675        (Align2::LEFT_TOP, cr.nw, i.left & i.top)
676    } else if possible.resize_right && possible.resize_top {
677        (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
678    } else {
679        // We're not in two directions, but it is still nice to tell the user
680        // we're resizable by painting the resize corner in the expected place
681        // (i.e. for windows only resizable in one direction):
682        if possible.resize_right || possible.resize_bottom {
683            (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
684        } else if possible.resize_left || possible.resize_bottom {
685            (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
686        } else if possible.resize_left || possible.resize_top {
687            (Align2::LEFT_TOP, cr.nw, i.left & i.top)
688        } else if possible.resize_right || possible.resize_top {
689            (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
690        } else {
691            return;
692        }
693    };
694
695    // Adjust the corner offset to accommodate for window rounding
696    let radius = radius as f32;
697    let offset =
698        ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
699
700    let stroke = if corner_response.drag {
701        ui.visuals().widgets.active.fg_stroke
702    } else if corner_response.hover {
703        ui.visuals().widgets.hovered.fg_stroke
704    } else {
705        window_frame.stroke
706    };
707
708    let fill_rect = outer_rect.shrink(window_frame.stroke.width);
709    let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
710    let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
711    let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner
712    crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
713}
714
715// ----------------------------------------------------------------------------
716
717/// Which sides can be resized?
718#[derive(Clone, Copy, Debug)]
719struct PossibleInteractions {
720    // Which sides can we drag to resize or move?
721    resize_left: bool,
722    resize_right: bool,
723    resize_top: bool,
724    resize_bottom: bool,
725}
726
727impl PossibleInteractions {
728    fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
729        let movable = area.is_enabled() && area.is_movable();
730        let resizable = resize
731            .is_resizable()
732            .and(area.is_enabled() && !is_collapsed);
733        let pivot = area.get_pivot();
734        Self {
735            resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
736            resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
737            resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
738            resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
739        }
740    }
741
742    pub fn resizable(&self) -> bool {
743        self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
744    }
745}
746
747/// Resizing the window edges.
748#[derive(Clone, Copy, Debug)]
749struct ResizeInteraction {
750    /// Outer rect (outside the stroke)
751    outer_rect: Rect,
752
753    window_frame: Frame,
754
755    left: SideResponse,
756    right: SideResponse,
757    top: SideResponse,
758    bottom: SideResponse,
759}
760
761/// A miniature version of `Response`, for each side of the window.
762#[derive(Clone, Copy, Debug, Default)]
763struct SideResponse {
764    hover: bool,
765    drag: bool,
766}
767
768impl SideResponse {
769    pub fn any(&self) -> bool {
770        self.hover || self.drag
771    }
772}
773
774impl std::ops::BitAnd for SideResponse {
775    type Output = Self;
776
777    fn bitand(self, rhs: Self) -> Self::Output {
778        Self {
779            hover: self.hover && rhs.hover,
780            drag: self.drag && rhs.drag,
781        }
782    }
783}
784
785impl std::ops::BitOrAssign for SideResponse {
786    fn bitor_assign(&mut self, rhs: Self) {
787        *self = Self {
788            hover: self.hover || rhs.hover,
789            drag: self.drag || rhs.drag,
790        };
791    }
792}
793
794impl ResizeInteraction {
795    pub fn set_cursor(&self, ctx: &Context) {
796        let left = self.left.any();
797        let right = self.right.any();
798        let top = self.top.any();
799        let bottom = self.bottom.any();
800
801        // TODO(emilk): use one-sided cursors for when we reached the min/max size.
802        if (left && top) || (right && bottom) {
803            ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
804        } else if (right && top) || (left && bottom) {
805            ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
806        } else if left || right {
807            ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
808        } else if bottom || top {
809            ctx.set_cursor_icon(CursorIcon::ResizeVertical);
810        }
811    }
812
813    pub fn any_hovered(&self) -> bool {
814        self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
815    }
816
817    pub fn any_dragged(&self) -> bool {
818        self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
819    }
820}
821
822fn resize_response(
823    resize_interaction: ResizeInteraction,
824    ctx: &Context,
825    margins: Vec2,
826    area_layer_id: LayerId,
827    area: &mut area::Prepared,
828    resize_id: Id,
829) {
830    let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
831        return;
832    };
833
834    if area.constrain() {
835        new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
836    }
837
838    // TODO(emilk): add this to a Window state instead as a command "move here next frame"
839    area.state_mut().set_left_top_pos(new_rect.left_top());
840
841    if resize_interaction.any_dragged() {
842        if let Some(mut state) = resize::State::load(ctx, resize_id) {
843            state.requested_size = Some(new_rect.size() - margins);
844            state.store(ctx, resize_id);
845        }
846    }
847
848    ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
849}
850
851/// Acts on outer rect (outside the stroke)
852fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
853    if !interaction.any_dragged() {
854        return None;
855    }
856
857    let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
858    let mut rect = interaction.outer_rect; // prevent drift
859
860    // Put the rect in the center of the stroke:
861    rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
862
863    if interaction.left.drag {
864        rect.min.x = pointer_pos.x;
865    } else if interaction.right.drag {
866        rect.max.x = pointer_pos.x;
867    }
868
869    if interaction.top.drag {
870        rect.min.y = pointer_pos.y;
871    } else if interaction.bottom.drag {
872        rect.max.y = pointer_pos.y;
873    }
874
875    // Return to having the rect outside the stroke:
876    rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
877
878    Some(rect.round_ui())
879}
880
881fn resize_interaction(
882    ctx: &Context,
883    possible: PossibleInteractions,
884    layer_id: LayerId,
885    outer_rect: Rect,
886    window_frame: Frame,
887) -> ResizeInteraction {
888    if !possible.resizable() {
889        return ResizeInteraction {
890            outer_rect,
891            window_frame,
892            left: Default::default(),
893            right: Default::default(),
894            top: Default::default(),
895            bottom: Default::default(),
896        };
897    }
898
899    // The rect that is in the middle of the stroke:
900    let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
901
902    let side_response = |rect, id| {
903        let response = ctx.create_widget(
904            WidgetRect {
905                layer_id,
906                id,
907                rect,
908                interact_rect: rect,
909                sense: Sense::drag(),
910                enabled: true,
911            },
912            true,
913        );
914        SideResponse {
915            hover: response.hovered(),
916            drag: response.dragged(),
917        }
918    };
919
920    let id = Id::new(layer_id).with("edge_drag");
921
922    let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
923    let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
924
925    let vetrtical_rect = |a: Pos2, b: Pos2| {
926        Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
927    };
928    let horizontal_rect = |a: Pos2, b: Pos2| {
929        Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
930    };
931    let corner_rect =
932        |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
933
934    // What are we dragging/hovering?
935    let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
936
937    // ----------------------------------------
938    // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority)
939
940    if possible.resize_right {
941        let response = side_response(
942            vetrtical_rect(rect.right_top(), rect.right_bottom()),
943            id.with("right"),
944        );
945        right |= response;
946    }
947    if possible.resize_left {
948        let response = side_response(
949            vetrtical_rect(rect.left_top(), rect.left_bottom()),
950            id.with("left"),
951        );
952        left |= response;
953    }
954    if possible.resize_bottom {
955        let response = side_response(
956            horizontal_rect(rect.left_bottom(), rect.right_bottom()),
957            id.with("bottom"),
958        );
959        bottom |= response;
960    }
961    if possible.resize_top {
962        let response = side_response(
963            horizontal_rect(rect.left_top(), rect.right_top()),
964            id.with("top"),
965        );
966        top |= response;
967    }
968
969    // ----------------------------------------
970    // Now check corners.
971    // We check any corner that has either side resizable,
972    // because we shrink the side resize handled by the corner width.
973    // Also, even if we can only change the width (or height) of a window,
974    // we show one of the corners as a grab-handle, so it makes sense that
975    // the whole corner is grabbable:
976
977    if possible.resize_right || possible.resize_bottom {
978        let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
979        if possible.resize_right {
980            right |= response;
981        }
982        if possible.resize_bottom {
983            bottom |= response;
984        }
985    }
986
987    if possible.resize_right || possible.resize_top {
988        let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
989        if possible.resize_right {
990            right |= response;
991        }
992        if possible.resize_top {
993            top |= response;
994        }
995    }
996
997    if possible.resize_left || possible.resize_bottom {
998        let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
999        if possible.resize_left {
1000            left |= response;
1001        }
1002        if possible.resize_bottom {
1003            bottom |= response;
1004        }
1005    }
1006
1007    if possible.resize_left || possible.resize_top {
1008        let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
1009        if possible.resize_left {
1010            left |= response;
1011        }
1012        if possible.resize_top {
1013            top |= response;
1014        }
1015    }
1016
1017    let interaction = ResizeInteraction {
1018        outer_rect,
1019        window_frame,
1020        left,
1021        right,
1022        top,
1023        bottom,
1024    };
1025    interaction.set_cursor(ctx);
1026    interaction
1027}
1028
1029/// Fill in parts of the window frame when we resize by dragging that part
1030fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) {
1031    use epaint::tessellator::path::add_circle_quadrant;
1032
1033    let visuals = if interaction.any_dragged() {
1034        ui.style().visuals.widgets.active
1035    } else if interaction.any_hovered() {
1036        ui.style().visuals.widgets.hovered
1037    } else {
1038        return;
1039    };
1040
1041    let [left, right, top, bottom]: [bool; 4];
1042
1043    if interaction.any_dragged() {
1044        left = interaction.left.drag;
1045        right = interaction.right.drag;
1046        top = interaction.top.drag;
1047        bottom = interaction.bottom.drag;
1048    } else {
1049        left = interaction.left.hover;
1050        right = interaction.right.hover;
1051        top = interaction.top.hover;
1052        bottom = interaction.bottom.hover;
1053    }
1054
1055    let cr = CornerRadiusF32::from(ui.visuals().window_corner_radius);
1056
1057    // Put the rect in the center of the fixed window stroke:
1058    let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1059
1060    // Make sure the inner part of the stroke is at a pixel boundary:
1061    let stroke = visuals.bg_stroke;
1062    let half_stroke = stroke.width / 2.0;
1063    let rect = rect
1064        .shrink(half_stroke)
1065        .round_to_pixels(ui.pixels_per_point())
1066        .expand(half_stroke);
1067
1068    let Rect { min, max } = rect;
1069
1070    let mut points = Vec::new();
1071
1072    if right && !bottom && !top {
1073        points.push(pos2(max.x, min.y + cr.ne));
1074        points.push(pos2(max.x, max.y - cr.se));
1075    }
1076    if right && bottom {
1077        points.push(pos2(max.x, min.y + cr.ne));
1078        points.push(pos2(max.x, max.y - cr.se));
1079        add_circle_quadrant(&mut points, pos2(max.x - cr.se, max.y - cr.se), cr.se, 0.0);
1080    }
1081    if bottom {
1082        points.push(pos2(max.x - cr.se, max.y));
1083        points.push(pos2(min.x + cr.sw, max.y));
1084    }
1085    if left && bottom {
1086        add_circle_quadrant(&mut points, pos2(min.x + cr.sw, max.y - cr.sw), cr.sw, 1.0);
1087    }
1088    if left {
1089        points.push(pos2(min.x, max.y - cr.sw));
1090        points.push(pos2(min.x, min.y + cr.nw));
1091    }
1092    if left && top {
1093        add_circle_quadrant(&mut points, pos2(min.x + cr.nw, min.y + cr.nw), cr.nw, 2.0);
1094    }
1095    if top {
1096        points.push(pos2(min.x + cr.nw, min.y));
1097        points.push(pos2(max.x - cr.ne, min.y));
1098    }
1099    if right && top {
1100        add_circle_quadrant(&mut points, pos2(max.x - cr.ne, min.y + cr.ne), cr.ne, 3.0);
1101        points.push(pos2(max.x, min.y + cr.ne));
1102        points.push(pos2(max.x, max.y - cr.se));
1103    }
1104
1105    ui.painter().add(Shape::line(points, stroke));
1106}
1107
1108// ----------------------------------------------------------------------------
1109
1110struct TitleBar {
1111    window_frame: Frame,
1112
1113    /// Prepared text in the title
1114    title_galley: Arc<Galley>,
1115
1116    /// Size of the title bar in an expanded state. This size become known only
1117    /// after expanding window and painting its content.
1118    ///
1119    /// Does not include the stroke, nor the separator line between the title bar and the window contents.
1120    inner_rect: Rect,
1121}
1122
1123impl TitleBar {
1124    fn new(
1125        ui: &Ui,
1126        title: WidgetText,
1127        show_close_button: bool,
1128        collapsible: bool,
1129        window_frame: Frame,
1130        title_bar_height_with_margin: f32,
1131    ) -> Self {
1132        if false {
1133            ui.ctx()
1134                .debug_painter()
1135                .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
1136        }
1137
1138        let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
1139
1140        let item_spacing = ui.spacing().item_spacing;
1141        let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
1142
1143        let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
1144
1145        let title_galley = title.into_galley(
1146            ui,
1147            Some(crate::TextWrapMode::Extend),
1148            f32::INFINITY,
1149            TextStyle::Heading,
1150        );
1151
1152        let minimum_width = if collapsible || show_close_button {
1153            // If at least one button is shown we make room for both buttons (since title should be centered):
1154            2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
1155        } else {
1156            left_pad + title_galley.size().x + left_pad
1157        };
1158        let min_inner_size = vec2(minimum_width, inner_height);
1159        let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
1160
1161        if false {
1162            ui.ctx()
1163                .debug_painter()
1164                .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
1165        }
1166
1167        Self {
1168            window_frame,
1169            title_galley,
1170            inner_rect: min_rect, // First estimate - will be refined later
1171        }
1172    }
1173
1174    /// Finishes painting of the title bar when the window content size already known.
1175    ///
1176    /// # Parameters
1177    ///
1178    /// - `ui`:
1179    /// - `outer_rect`:
1180    /// - `content_response`: if `None`, window is collapsed at this frame, otherwise contains
1181    ///   a result of rendering the window content
1182    /// - `open`: if `None`, no "Close" button will be rendered, otherwise renders and processes
1183    ///   the "Close" button and writes a `false` if window was closed
1184    /// - `collapsing`: holds the current expanding state. Can be changed by double click on the
1185    ///   title if `collapsible` is `true`
1186    /// - `collapsible`: if `true`, double click on the title bar will be handled for a change
1187    ///   of `collapsing` state
1188    fn ui(
1189        self,
1190        ui: &mut Ui,
1191        content_response: &Option<Response>,
1192        open: Option<&mut bool>,
1193        collapsing: &mut CollapsingState,
1194        collapsible: bool,
1195    ) {
1196        let window_frame = self.window_frame;
1197        let title_inner_rect = self.inner_rect;
1198
1199        if false {
1200            ui.ctx()
1201                .debug_painter()
1202                .debug_rect(self.inner_rect, Color32::RED, "TitleBar");
1203        }
1204
1205        if collapsible {
1206            // Show collapse-button:
1207            let button_center = Align2::LEFT_CENTER
1208                .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1209                .center();
1210            let button_size = Vec2::splat(ui.spacing().icon_width);
1211            let button_rect = Rect::from_center_size(button_center, button_size);
1212            let button_rect = button_rect.round_ui();
1213
1214            ui.allocate_new_ui(UiBuilder::new().max_rect(button_rect), |ui| {
1215                collapsing.show_default_button_with_size(ui, button_size);
1216            });
1217        }
1218
1219        if let Some(open) = open {
1220            // Add close button now that we know our full width:
1221            if self.close_button_ui(ui).clicked() {
1222                *open = false;
1223            }
1224        }
1225
1226        let text_pos =
1227            emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
1228                .left_top();
1229        let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
1230        ui.painter().galley(
1231            text_pos,
1232            self.title_galley.clone(),
1233            ui.visuals().text_color(),
1234        );
1235
1236        if let Some(content_response) = &content_response {
1237            // Paint separator between title and content:
1238            let content_rect = content_response.rect;
1239            if false {
1240                ui.ctx()
1241                    .debug_painter()
1242                    .debug_rect(content_rect, Color32::RED, "content_rect");
1243            }
1244            let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
1245
1246            // To verify the sanity of this, use a very wide window stroke
1247            ui.painter()
1248                .hline(title_inner_rect.x_range(), y, window_frame.stroke);
1249        }
1250
1251        // Don't cover the close- and collapse buttons:
1252        let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
1253
1254        if false {
1255            ui.ctx().debug_painter().debug_rect(
1256                double_click_rect,
1257                Color32::GREEN,
1258                "double_click_rect",
1259            );
1260        }
1261
1262        let id = ui.unique_id().with("__window_title_bar");
1263
1264        if ui
1265            .interact(double_click_rect, id, Sense::click())
1266            .double_clicked()
1267            && collapsible
1268        {
1269            collapsing.toggle(ui);
1270        }
1271    }
1272
1273    /// Paints the "Close" button at the right side of the title bar
1274    /// and processes clicks on it.
1275    ///
1276    /// The button is square and its size is determined by the
1277    /// [`crate::style::Spacing::icon_width`] setting.
1278    fn close_button_ui(&self, ui: &mut Ui) -> Response {
1279        let button_center = Align2::RIGHT_CENTER
1280            .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
1281            .center();
1282        let button_size = Vec2::splat(ui.spacing().icon_width);
1283        let button_rect = Rect::from_center_size(button_center, button_size);
1284        let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
1285        close_button(ui, button_rect)
1286    }
1287}
1288
1289/// Paints the "Close" button of the window and processes clicks on it.
1290///
1291/// The close button is just an `X` symbol painted by a current stroke
1292/// for foreground elements (such as a label text).
1293///
1294/// # Parameters
1295/// - `ui`:
1296/// - `rect`: The rectangular area to fit the button in
1297///
1298/// Returns the result of a click on a button if it was pressed
1299fn close_button(ui: &mut Ui, rect: Rect) -> Response {
1300    let close_id = ui.auto_id_with("window_close_button");
1301    let response = ui.interact(rect, close_id, Sense::click());
1302    response
1303        .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window"));
1304
1305    ui.expand_to_include_rect(response.rect);
1306
1307    let visuals = ui.style().interact(&response);
1308    let rect = rect.shrink(2.0).expand(visuals.expansion);
1309    let stroke = visuals.fg_stroke;
1310    ui.painter() // paints \
1311        .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1312    ui.painter() // paints /
1313        .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1314    response
1315}