Skip to main content

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, ScrollSource};
12use super::{Area, Frame, Resize, ScrollArea, area, resize};
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    /// Construct a [`Window`] that follows the given viewport.
74    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            .. // A lot of things not implemented yet
86        } = 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    /// Assign a unique id to the Window. Required if the title changes, or is shared with another window.
109    #[inline]
110    pub fn id(mut self, id: Id) -> Self {
111        self.area = self.area.id(id);
112        self
113    }
114
115    /// Call this to add a close-button to the window title bar.
116    ///
117    /// * If `*open == false`, the window will not be visible.
118    /// * If `*open == true`, the window will have a close button.
119    /// * If the close button is pressed, `*open` will be set to `false`.
120    #[inline]
121    pub fn open(mut self, open: &'open mut bool) -> Self {
122        self.open = Some(open);
123        self
124    }
125
126    /// If `false` the window will be grayed out and non-interactive.
127    #[inline]
128    pub fn enabled(mut self, enabled: bool) -> Self {
129        self.area = self.area.enabled(enabled);
130        self
131    }
132
133    /// If false, clicks goes straight through to what is behind us.
134    ///
135    /// Can be used for semi-invisible areas that the user should be able to click through.
136    ///
137    /// Default: `true`.
138    #[inline]
139    pub fn interactable(mut self, interactable: bool) -> Self {
140        self.area = self.area.interactable(interactable);
141        self
142    }
143
144    /// If `false` the window will be immovable.
145    #[inline]
146    pub fn movable(mut self, movable: bool) -> Self {
147        self.area = self.area.movable(movable);
148        self
149    }
150
151    /// `order(Order::Foreground)` for a Window that should always be on top
152    #[inline]
153    pub fn order(mut self, order: Order) -> Self {
154        self.area = self.area.order(order);
155        self
156    }
157
158    /// If `true`, quickly fade in the `Window` when it first appears.
159    ///
160    /// Default: `true`.
161    #[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    /// If `true`, quickly fade out the `Window` when it closes.
168    ///
169    /// This only works if you use [`Self::open`] to close the window.
170    ///
171    /// Default: `true`.
172    #[inline]
173    pub fn fade_out(mut self, fade_out: bool) -> Self {
174        self.fade_out = fade_out;
175        self
176    }
177
178    /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
179    // TODO(emilk): I'm not sure this is a good interface for this.
180    #[inline]
181    pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
182        mutate(&mut self);
183        self
184    }
185
186    /// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
187    // TODO(emilk): I'm not sure this is a good interface for this.
188    #[inline]
189    pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
190        self.resize = mutate(self.resize);
191        self
192    }
193
194    /// Change the background color, margins, etc.
195    #[inline]
196    pub fn frame(mut self, frame: Frame) -> Self {
197        self.frame = Some(frame);
198        self
199    }
200
201    /// Set minimum width of the window.
202    #[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    /// Set minimum height of the window.
209    #[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    /// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`.
216    #[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    /// Set maximum width of the window.
223    #[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    /// Set maximum height of the window.
230    #[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    /// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`.
237    #[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    /// Set current position of the window.
244    /// If the window is movable it is up to you to keep track of where it moved to!
245    #[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    /// Set initial position of the window.
252    #[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    /// Sets the window position and prevents it from being dragged around.
259    #[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    /// Constrains this window to [`Context::screen_rect`].
266    ///
267    /// To change the area to constrain to, use [`Self::constrain_to`].
268    ///
269    /// Default: `true`.
270    #[inline]
271    pub fn constrain(mut self, constrain: bool) -> Self {
272        self.area = self.area.constrain(constrain);
273        self
274    }
275
276    /// Constrain the movement of the window to the given rectangle.
277    ///
278    /// For instance: `.constrain_to(ctx.screen_rect())`.
279    #[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    /// Where the "root" of the window is.
286    ///
287    /// For instance, if you set this to [`Align2::RIGHT_TOP`]
288    /// then [`Self::fixed_pos`] will set the position of the right-top
289    /// corner of the window.
290    ///
291    /// Default: [`Align2::LEFT_TOP`].
292    #[inline]
293    pub fn pivot(mut self, pivot: Align2) -> Self {
294        self.area = self.area.pivot(pivot);
295        self
296    }
297
298    /// Set anchor and distance.
299    ///
300    /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
301    /// in the right-top corner of the screen".
302    ///
303    /// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
304    /// would move the window left and down from the given anchor.
305    ///
306    /// Anchoring also makes the window immovable.
307    ///
308    /// It is an error to set both an anchor and a position.
309    #[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    /// Set initial collapsed state of the window
316    #[inline]
317    pub fn default_open(mut self, default_open: bool) -> Self {
318        self.default_open = default_open;
319        self
320    }
321
322    /// Set initial size of the window.
323    #[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    /// Set initial width of the window.
332    #[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    /// Set initial height of the window.
340    #[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    /// Sets the window size and prevents it from being resized by dragging its edges.
348    #[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    /// Set initial position and size of the window.
355    pub fn default_rect(self, rect: Rect) -> Self {
356        self.default_pos(rect.min).default_size(rect.size())
357    }
358
359    /// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
360    pub fn fixed_rect(self, rect: Rect) -> Self {
361        self.fixed_pos(rect.min).fixed_size(rect.size())
362    }
363
364    /// Can the user resize the window by dragging its edges?
365    ///
366    /// Note that even if you set this to `false` the window may still auto-resize.
367    ///
368    /// You can set the window to only be resizable in one direction by using
369    /// e.g. `[true, false]` as the argument,
370    /// making the window only resizable in the x-direction.
371    ///
372    /// Default is `true`.
373    #[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    /// Can the window be collapsed by clicking on its title?
381    #[inline]
382    pub fn collapsible(mut self, collapsible: bool) -> Self {
383        self.collapsible = collapsible;
384        self
385    }
386
387    /// Show title bar on top of the window?
388    /// If `false`, the window will not be collapsible nor have a close-button.
389    #[inline]
390    pub fn title_bar(mut self, title_bar: bool) -> Self {
391        self.with_title_bar = title_bar;
392        self
393    }
394
395    /// Not resizable, just takes the size of its contents.
396    /// Also disabled scrolling.
397    /// Text will not wrap, but will instead make your window width expand.
398    #[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    /// Enable/disable horizontal/vertical scrolling. `false` by default.
406    ///
407    /// You can pass in `false`, `true`, `[false, true]` etc.
408    #[inline]
409    pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
410        self.scroll = self.scroll.scroll(scroll);
411        self
412    }
413
414    /// Enable/disable horizontal scrolling. `false` by default.
415    #[inline]
416    pub fn hscroll(mut self, hscroll: bool) -> Self {
417        self.scroll = self.scroll.hscroll(hscroll);
418        self
419    }
420
421    /// Enable/disable vertical scrolling. `false` by default.
422    #[inline]
423    pub fn vscroll(mut self, vscroll: bool) -> Self {
424        self.scroll = self.scroll.vscroll(vscroll);
425        self
426    }
427
428    /// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
429    ///
430    /// See [`ScrollArea::drag_to_scroll`] for more.
431    #[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    /// Sets the [`ScrollBarVisibility`] of the window.
441    #[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    /// Returns `None` if the window is not open (if [`Window::open`] was called with `&mut false`).
450    /// Returns `Some(InnerResponse { inner: None })` if the window is collapsed.
451    #[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); // We resize it manually
505        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        // Calculate roughly how much larger the full window inner size is compared to the content rect
513        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            // Prevent window from becoming larger than the constrain rect.
534            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        // First check for resize to avoid frame delay:
543        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            // `Area` already takes care of fade-in animations,
570            // so we only need to handle fade-out animations here.
571        } else if fade_out {
572            area_content_ui.multiply_opacity(opacity);
573        }
574
575        let content_inner = {
576            // BEGIN FRAME --------------------------------
577            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()); // Prevent making window smaller than title bar width
593
594                frame.content_ui.set_min_size(title_bar.inner_rect.size());
595
596                // Skip the title bar (and separator):
597                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            // Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window.
627            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            // END FRAME --------------------------------
645
646            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        // We're not in two directions, but it is still nice to tell the user
725        // we're resizable by painting the resize corner in the expected place
726        // (i.e. for windows only resizable in one direction):
727        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    // Adjust the corner offset to accommodate for window rounding
741    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()); // move away from corner
757    crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
758}
759
760// ----------------------------------------------------------------------------
761
762/// Which sides can be resized?
763#[derive(Clone, Copy, Debug)]
764struct PossibleInteractions {
765    // Which sides can we drag to resize or move?
766    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/// Resizing the window edges.
793#[derive(Clone, Copy, Debug)]
794struct ResizeInteraction {
795    /// Outer rect (outside the stroke)
796    outer_rect: Rect,
797
798    window_frame: Frame,
799
800    left: SideResponse,
801    right: SideResponse,
802    top: SideResponse,
803    bottom: SideResponse,
804}
805
806/// A miniature version of `Response`, for each side of the window.
807#[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        // TODO(emilk): use one-sided cursors for when we reached the min/max size.
847        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    // TODO(emilk): add this to a Window state instead as a command "move here next frame"
884    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
896/// Acts on outer rect (outside the stroke)
897fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
898    // Used to prevent drift
899    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; // prevent drift
915
916    // Put the rect in the center of the stroke:
917    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    // Return to having the rect outside the stroke:
932    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    // The rect that is in the middle of the stroke:
957    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, // Don't use Sense::drag() since we don't want these to be focusable
969                enabled: true,
970            },
971            true,
972            InteractOptions {
973                // We call this multiple times.
974                // First to read the result (to avoid frame delay)
975                // and the second time to move it to the top, above the window contents.
976                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    // What are we dragging/hovering?
1005    let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4];
1006
1007    // ----------------------------------------
1008    // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority)
1009
1010    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    // ----------------------------------------
1040    // Now check corners.
1041    // We check any corner that has either side resizable,
1042    // because we shrink the side resize handled by the corner width.
1043    // Also, even if we can only change the width (or height) of a window,
1044    // we show one of the corners as a grab-handle, so it makes sense that
1045    // the whole corner is grabbable:
1046
1047    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
1099/// Fill in parts of the window frame when we resize by dragging that part
1100fn 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    // Put the rect in the center of the fixed window stroke:
1128    let rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
1129
1130    // Make sure the inner part of the stroke is at a pixel boundary:
1131    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
1178// ----------------------------------------------------------------------------
1179
1180struct TitleBar {
1181    window_frame: Frame,
1182
1183    /// Prepared text in the title
1184    title_galley: Arc<Galley>,
1185
1186    /// Size of the title bar in an expanded state. This size become known only
1187    /// after expanding window and painting its content.
1188    ///
1189    /// Does not include the stroke, nor the separator line between the title bar and the window contents.
1190    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(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
1213
1214        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            // If at least one button is shown we make room for both buttons (since title should be centered):
1223            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, // First estimate - will be refined later
1239        }
1240    }
1241
1242    /// Finishes painting of the title bar when the window content size already known.
1243    ///
1244    /// # Parameters
1245    ///
1246    /// - `ui`:
1247    /// - `outer_rect`:
1248    /// - `content_response`: if `None`, window is collapsed at this frame, otherwise contains
1249    ///   a result of rendering the window content
1250    /// - `open`: if `None`, no "Close" button will be rendered, otherwise renders and processes
1251    ///   the "Close" button and writes a `false` if window was closed
1252    /// - `collapsing`: holds the current expanding state. Can be changed by double click on the
1253    ///   title if `collapsible` is `true`
1254    /// - `collapsible`: if `true`, double click on the title bar will be handled for a change
1255    ///   of `collapsing` state
1256    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            // Show collapse-button:
1274            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            // Add close button now that we know our full width:
1288            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            // Paint separator between title and content:
1305            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            // To verify the sanity of this, use a very wide window stroke
1313            ui.painter()
1314                .hline(title_inner_rect.x_range(), y, window_frame.stroke);
1315        }
1316
1317        // Don't cover the close- and collapse buttons:
1318        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    /// Paints the "Close" button at the right side of the title bar
1337    /// and processes clicks on it.
1338    ///
1339    /// The button is square and its size is determined by the
1340    /// [`crate::style::Spacing::icon_width`] setting.
1341    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
1352/// Paints the "Close" button of the window and processes clicks on it.
1353///
1354/// The close button is just an `X` symbol painted by a current stroke
1355/// for foreground elements (such as a label text).
1356///
1357/// # Parameters
1358/// - `ui`:
1359/// - `rect`: The rectangular area to fit the button in
1360///
1361/// Returns the result of a click on a button if it was pressed
1362fn 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() // paints \
1374        .line_segment([rect.left_top(), rect.right_bottom()], stroke);
1375    ui.painter() // paints /
1376        .line_segment([rect.right_top(), rect.left_bottom()], stroke);
1377    response
1378}