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