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_mut(|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 = resize_interaction(
509            ctx,
510            possible,
511            area.id(),
512            area_layer_id,
513            last_frame_outer_rect,
514            window_frame,
515        );
516
517        {
518            let margins = window_frame.total_margin().sum()
519                + vec2(0.0, title_bar_height_with_margin + title_content_spacing);
520
521            resize_response(
522                resize_interaction,
523                ctx,
524                margins,
525                area_layer_id,
526                &mut area,
527                resize_id,
528            );
529        }
530
531        let mut area_content_ui = area.content_ui(ctx);
532        if is_open {
533            // `Area` already takes care of fade-in animations,
534            // so we only need to handle fade-out animations here.
535        } else if fade_out {
536            area_content_ui.multiply_opacity(opacity);
537        }
538
539        let content_inner = {
540            // BEGIN FRAME --------------------------------
541            let mut frame = window_frame.begin(&mut area_content_ui);
542
543            let show_close_button = open.is_some();
544
545            let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
546
547            let title_bar = if with_title_bar {
548                let title_bar = TitleBar::new(
549                    &frame.content_ui,
550                    title,
551                    show_close_button,
552                    collapsible,
553                    window_frame,
554                    title_bar_height_with_margin,
555                );
556                resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width
557
558                frame.content_ui.set_min_size(title_bar.inner_rect.size());
559
560                // Skip the title bar (and separator):
561                if is_collapsed {
562                    frame.content_ui.add_space(title_bar.inner_rect.height());
563                } else {
564                    frame.content_ui.add_space(
565                        title_bar.inner_rect.height()
566                            + title_content_spacing
567                            + window_frame.inner_margin.sum().y,
568                    );
569                }
570
571                Some(title_bar)
572            } else {
573                None
574            };
575
576            let (content_inner, content_response) = collapsing
577                .show_body_unindented(&mut frame.content_ui, |ui| {
578                    resize.show(ui, |ui| {
579                        if scroll.is_any_scroll_enabled() {
580                            scroll.show(ui, add_contents).inner
581                        } else {
582                            add_contents(ui)
583                        }
584                    })
585                })
586                .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
587
588            let outer_rect = frame.end(&mut area_content_ui).rect;
589            paint_resize_corner(
590                &area_content_ui,
591                &possible,
592                outer_rect,
593                &window_frame,
594                resize_interaction,
595            );
596
597            // END FRAME --------------------------------
598
599            if let Some(mut title_bar) = title_bar {
600                title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
601                title_bar.inner_rect.max.y =
602                    title_bar.inner_rect.min.y + title_bar_height_with_margin;
603
604                if on_top && area_content_ui.visuals().window_highlight_topmost {
605                    let mut round =
606                        window_frame.corner_radius - window_frame.stroke.width.round() as u8;
607
608                    if !is_collapsed {
609                        round.se = 0;
610                        round.sw = 0;
611                    }
612
613                    area_content_ui.painter().set(
614                        *where_to_put_header_background,
615                        RectShape::filled(title_bar.inner_rect, round, header_color),
616                    );
617                }
618
619                if false {
620                    ctx.debug_painter().debug_rect(
621                        title_bar.inner_rect,
622                        Color32::LIGHT_BLUE,
623                        "title_bar.rect",
624                    );
625                }
626
627                title_bar.ui(
628                    &mut area_content_ui,
629                    &content_response,
630                    open.as_deref_mut(),
631                    &mut collapsing,
632                    collapsible,
633                );
634            }
635
636            collapsing.store(ctx);
637
638            paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
639
640            content_inner
641        };
642
643        let full_response = area.end(ctx, area_content_ui);
644
645        if full_response.should_close()
646            && let Some(open) = open
647        {
648            *open = false;
649        }
650
651        let inner_response = InnerResponse {
652            inner: content_inner,
653            response: full_response,
654        };
655        Some(inner_response)
656    }
657}
658
659fn paint_resize_corner(
660    ui: &Ui,
661    possible: &PossibleInteractions,
662    outer_rect: Rect,
663    window_frame: &Frame,
664    i: ResizeInteraction,
665) {
666    let cr = window_frame.corner_radius;
667
668    let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
669        (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
670    } else if possible.resize_left && possible.resize_bottom {
671        (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
672    } else if possible.resize_left && possible.resize_top {
673        (Align2::LEFT_TOP, cr.nw, i.left & i.top)
674    } else if possible.resize_right && possible.resize_top {
675        (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
676    } else {
677        // We're not in two directions, but it is still nice to tell the user
678        // we're resizable by painting the resize corner in the expected place
679        // (i.e. for windows only resizable in one direction):
680        if possible.resize_right || possible.resize_bottom {
681            (Align2::RIGHT_BOTTOM, cr.se, i.right & i.bottom)
682        } else if possible.resize_left || possible.resize_bottom {
683            (Align2::LEFT_BOTTOM, cr.sw, i.left & i.bottom)
684        } else if possible.resize_left || possible.resize_top {
685            (Align2::LEFT_TOP, cr.nw, i.left & i.top)
686        } else if possible.resize_right || possible.resize_top {
687            (Align2::RIGHT_TOP, cr.ne, i.right & i.top)
688        } else {
689            return;
690        }
691    };
692
693    // Adjust the corner offset to accommodate for window rounding
694    let radius = radius as f32;
695    let offset =
696        ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
697
698    let stroke = if corner_response.drag {
699        ui.visuals().widgets.active.fg_stroke
700    } else if corner_response.hover {
701        ui.visuals().widgets.hovered.fg_stroke
702    } else {
703        window_frame.stroke
704    };
705
706    let fill_rect = outer_rect.shrink(window_frame.stroke.width);
707    let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
708    let corner_rect = corner.align_size_within_rect(corner_size, fill_rect);
709    let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner
710    crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
711}
712
713// ----------------------------------------------------------------------------
714
715/// Which sides can be resized?
716#[derive(Clone, Copy, Debug)]
717struct PossibleInteractions {
718    // Which sides can we drag to resize or move?
719    resize_left: bool,
720    resize_right: bool,
721    resize_top: bool,
722    resize_bottom: bool,
723}
724
725impl PossibleInteractions {
726    fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
727        let movable = area.is_enabled() && area.is_movable();
728        let resizable = resize
729            .is_resizable()
730            .and(area.is_enabled() && !is_collapsed);
731        let pivot = area.get_pivot();
732        Self {
733            resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
734            resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
735            resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
736            resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
737        }
738    }
739
740    pub fn resizable(&self) -> bool {
741        self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
742    }
743}
744
745/// Resizing the window edges.
746#[derive(Clone, Copy, Debug)]
747struct ResizeInteraction {
748    /// Outer rect (outside the stroke)
749    outer_rect: Rect,
750
751    window_frame: Frame,
752
753    left: SideResponse,
754    right: SideResponse,
755    top: SideResponse,
756    bottom: SideResponse,
757}
758
759/// A miniature version of `Response`, for each side of the window.
760#[derive(Clone, Copy, Debug, Default)]
761struct SideResponse {
762    hover: bool,
763    drag: bool,
764}
765
766impl SideResponse {
767    pub fn any(&self) -> bool {
768        self.hover || self.drag
769    }
770}
771
772impl std::ops::BitAnd for SideResponse {
773    type Output = Self;
774
775    fn bitand(self, rhs: Self) -> Self::Output {
776        Self {
777            hover: self.hover && rhs.hover,
778            drag: self.drag && rhs.drag,
779        }
780    }
781}
782
783impl std::ops::BitOrAssign for SideResponse {
784    fn bitor_assign(&mut self, rhs: Self) {
785        *self = Self {
786            hover: self.hover || rhs.hover,
787            drag: self.drag || rhs.drag,
788        };
789    }
790}
791
792impl ResizeInteraction {
793    pub fn set_cursor(&self, ctx: &Context) {
794        let left = self.left.any();
795        let right = self.right.any();
796        let top = self.top.any();
797        let bottom = self.bottom.any();
798
799        // TODO(emilk): use one-sided cursors for when we reached the min/max size.
800        if (left && top) || (right && bottom) {
801            ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
802        } else if (right && top) || (left && bottom) {
803            ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
804        } else if left || right {
805            ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
806        } else if bottom || top {
807            ctx.set_cursor_icon(CursorIcon::ResizeVertical);
808        }
809    }
810
811    pub fn any_hovered(&self) -> bool {
812        self.left.hover || self.right.hover || self.top.hover || self.bottom.hover
813    }
814
815    pub fn any_dragged(&self) -> bool {
816        self.left.drag || self.right.drag || self.top.drag || self.bottom.drag
817    }
818}
819
820fn resize_response(
821    resize_interaction: ResizeInteraction,
822    ctx: &Context,
823    margins: Vec2,
824    area_layer_id: LayerId,
825    area: &mut area::Prepared,
826    resize_id: Id,
827) {
828    let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
829        return;
830    };
831
832    if area.constrain() {
833        new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
834    }
835
836    // TODO(emilk): add this to a Window state instead as a command "move here next frame"
837    area.state_mut().set_left_top_pos(new_rect.left_top());
838
839    if resize_interaction.any_dragged()
840        && let Some(mut state) = resize::State::load(ctx, resize_id)
841    {
842        state.requested_size = Some(new_rect.size() - margins);
843        state.store(ctx, resize_id);
844    }
845
846    ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
847}
848
849/// Acts on outer rect (outside the stroke)
850fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
851    if !interaction.any_dragged() {
852        return None;
853    }
854
855    let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
856    let mut rect = interaction.outer_rect; // prevent drift
857
858    // Put the rect in the center of the stroke:
859    rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
860
861    if interaction.left.drag {
862        rect.min.x = pointer_pos.x;
863    } else if interaction.right.drag {
864        rect.max.x = pointer_pos.x;
865    }
866
867    if interaction.top.drag {
868        rect.min.y = pointer_pos.y;
869    } else if interaction.bottom.drag {
870        rect.max.y = pointer_pos.y;
871    }
872
873    // Return to having the rect outside the stroke:
874    rect = rect.expand(interaction.window_frame.stroke.width / 2.0);
875
876    Some(rect.round_ui())
877}
878
879fn resize_interaction(
880    ctx: &Context,
881    possible: PossibleInteractions,
882    _accessibility_parent: Id,
883    layer_id: LayerId,
884    outer_rect: Rect,
885    window_frame: Frame,
886) -> ResizeInteraction {
887    if !possible.resizable() {
888        return ResizeInteraction {
889            outer_rect,
890            window_frame,
891            left: Default::default(),
892            right: Default::default(),
893            top: Default::default(),
894            bottom: Default::default(),
895        };
896    }
897
898    // The rect that is in the middle of the stroke:
899    let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
900
901    let side_response = |rect, id| {
902        #[cfg(feature = "accesskit")]
903        ctx.register_accesskit_parent(id, _accessibility_parent);
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}