egui/containers/
window.rs

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