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