egui/containers/
area.rs

1//! Area is a [`Ui`] that has no parent, it floats on the background.
2//! It has no frame or own size. It is potentially movable.
3//! It is the foundation for windows and popups.
4
5use emath::GuiRounding as _;
6
7use crate::{
8    emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
9    Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
10};
11
12/// State of an [`Area`] that is persisted between frames.
13///
14/// Areas back [`crate::Window`]s and other floating containers,
15/// like tooltips and the popups of [`crate::ComboBox`].
16#[derive(Clone, Copy, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18pub struct AreaState {
19    /// Last known position of the pivot.
20    pub pivot_pos: Option<Pos2>,
21
22    /// The anchor point of the area, i.e. where on the area the [`Self::pivot_pos`] refers to.
23    pub pivot: Align2,
24
25    /// Last known size.
26    ///
27    /// Area size is intentionally NOT persisted between sessions,
28    /// so that a bad tooltip or menu size won't be remembered forever.
29    /// A resizable [`crate::Window`] remembers the size the user picked using
30    /// the state in the [`crate::Resize`] container.
31    #[cfg_attr(feature = "serde", serde(skip))]
32    pub size: Option<Vec2>,
33
34    /// If false, clicks goes straight through to what is behind us. Useful for tooltips etc.
35    pub interactable: bool,
36
37    /// At what time was this area first shown?
38    ///
39    /// Used to fade in the area.
40    #[cfg_attr(feature = "serde", serde(skip))]
41    pub last_became_visible_at: Option<f64>,
42}
43
44impl Default for AreaState {
45    fn default() -> Self {
46        Self {
47            pivot_pos: None,
48            pivot: Align2::LEFT_TOP,
49            size: None,
50            interactable: true,
51            last_became_visible_at: None,
52        }
53    }
54}
55
56impl AreaState {
57    /// Load the state of an [`Area`] from memory.
58    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
59        // TODO(emilk): Area state is not currently stored in `Memory::data`, but maybe it should be?
60        ctx.memory(|mem| mem.areas().get(id).copied())
61    }
62
63    /// The left top positions of the area.
64    pub fn left_top_pos(&self) -> Pos2 {
65        let pivot_pos = self.pivot_pos.unwrap_or_default();
66        let size = self.size.unwrap_or_default();
67        pos2(
68            pivot_pos.x - self.pivot.x().to_factor() * size.x,
69            pivot_pos.y - self.pivot.y().to_factor() * size.y,
70        )
71        .round_ui()
72    }
73
74    /// Move the left top positions of the area.
75    pub fn set_left_top_pos(&mut self, pos: Pos2) {
76        let size = self.size.unwrap_or_default();
77        self.pivot_pos = Some(pos2(
78            pos.x + self.pivot.x().to_factor() * size.x,
79            pos.y + self.pivot.y().to_factor() * size.y,
80        ));
81    }
82
83    /// Where the area is on screen.
84    pub fn rect(&self) -> Rect {
85        let size = self.size.unwrap_or_default();
86        Rect::from_min_size(self.left_top_pos(), size).round_ui()
87    }
88}
89
90/// An area on the screen that can be moved by dragging.
91///
92/// This forms the base of the [`crate::Window`] container.
93///
94/// ```
95/// # egui::__run_test_ctx(|ctx| {
96/// egui::Area::new(egui::Id::new("my_area"))
97///     .fixed_pos(egui::pos2(32.0, 32.0))
98///     .show(ctx, |ui| {
99///         ui.label("Floating text!");
100///     });
101/// # });
102/// ```
103///
104/// The previous rectangle used by this area can be obtained through [`crate::Memory::area_rect()`].
105#[must_use = "You should call .show()"]
106#[derive(Clone, Copy, Debug)]
107pub struct Area {
108    pub(crate) id: Id,
109    kind: UiKind,
110    sense: Option<Sense>,
111    movable: bool,
112    interactable: bool,
113    enabled: bool,
114    constrain: bool,
115    constrain_rect: Option<Rect>,
116    order: Order,
117    default_pos: Option<Pos2>,
118    default_size: Vec2,
119    pivot: Align2,
120    anchor: Option<(Align2, Vec2)>,
121    new_pos: Option<Pos2>,
122    fade_in: bool,
123}
124
125impl WidgetWithState for Area {
126    type State = AreaState;
127}
128
129impl Area {
130    /// The `id` must be globally unique.
131    pub fn new(id: Id) -> Self {
132        Self {
133            id,
134            kind: UiKind::GenericArea,
135            sense: None,
136            movable: true,
137            interactable: true,
138            constrain: true,
139            constrain_rect: None,
140            enabled: true,
141            order: Order::Middle,
142            default_pos: None,
143            default_size: Vec2::NAN,
144            new_pos: None,
145            pivot: Align2::LEFT_TOP,
146            anchor: None,
147            fade_in: true,
148        }
149    }
150
151    /// Let's you change the `id` that you assigned in [`Self::new`].
152    ///
153    /// The `id` must be globally unique.
154    #[inline]
155    pub fn id(mut self, id: Id) -> Self {
156        self.id = id;
157        self
158    }
159
160    /// Change the [`UiKind`] of the arena.
161    ///
162    /// Default to [`UiKind::GenericArea`].
163    #[inline]
164    pub fn kind(mut self, kind: UiKind) -> Self {
165        self.kind = kind;
166        self
167    }
168
169    pub fn layer(&self) -> LayerId {
170        LayerId::new(self.order, self.id)
171    }
172
173    /// If false, no content responds to click
174    /// and widgets will be shown grayed out.
175    /// You won't be able to move the window.
176    /// Default: `true`.
177    #[inline]
178    pub fn enabled(mut self, enabled: bool) -> Self {
179        self.enabled = enabled;
180        self
181    }
182
183    /// Moveable by dragging the area?
184    #[inline]
185    pub fn movable(mut self, movable: bool) -> Self {
186        self.movable = movable;
187        self.interactable |= movable;
188        self
189    }
190
191    pub fn is_enabled(&self) -> bool {
192        self.enabled
193    }
194
195    pub fn is_movable(&self) -> bool {
196        self.movable && self.enabled
197    }
198
199    /// If false, clicks goes straight through to what is behind us.
200    ///
201    /// Can be used for semi-invisible areas that the user should be able to click through.
202    ///
203    /// Default: `true`.
204    #[inline]
205    pub fn interactable(mut self, interactable: bool) -> Self {
206        self.interactable = interactable;
207        self.movable &= interactable;
208        self
209    }
210
211    /// Explicitly set a sense.
212    ///
213    /// If not set, this will default to `Sense::drag()` if movable, `Sense::click()` if interactable, and `Sense::hover()` otherwise.
214    #[inline]
215    pub fn sense(mut self, sense: Sense) -> Self {
216        self.sense = Some(sense);
217        self
218    }
219
220    /// `order(Order::Foreground)` for an Area that should always be on top
221    #[inline]
222    pub fn order(mut self, order: Order) -> Self {
223        self.order = order;
224        self
225    }
226
227    #[inline]
228    pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
229        self.default_pos = Some(default_pos.into());
230        self
231    }
232
233    /// The size used for the [`Ui::max_rect`] the first frame.
234    ///
235    /// Text will wrap at this width, and images that expand to fill the available space
236    /// will expand to this size.
237    ///
238    /// If the contents are smaller than this size, the area will shrink to fit the contents.
239    /// If the contents overflow, the area will grow.
240    ///
241    /// If not set, [`crate::style::Spacing::default_area_size`] will be used.
242    #[inline]
243    pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
244        self.default_size = default_size.into();
245        self
246    }
247
248    /// See [`Self::default_size`].
249    #[inline]
250    pub fn default_width(mut self, default_width: f32) -> Self {
251        self.default_size.x = default_width;
252        self
253    }
254
255    /// See [`Self::default_size`].
256    #[inline]
257    pub fn default_height(mut self, default_height: f32) -> Self {
258        self.default_size.y = default_height;
259        self
260    }
261
262    /// Positions the window and prevents it from being moved
263    #[inline]
264    pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
265        self.new_pos = Some(fixed_pos.into());
266        self.movable = false;
267        self
268    }
269
270    /// Constrains this area to [`Context::screen_rect`]?
271    ///
272    /// Default: `true`.
273    #[inline]
274    pub fn constrain(mut self, constrain: bool) -> Self {
275        self.constrain = constrain;
276        self
277    }
278
279    /// Constrain the movement of the window to the given rectangle.
280    ///
281    /// For instance: `.constrain_to(ctx.screen_rect())`.
282    #[inline]
283    pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
284        self.constrain = true;
285        self.constrain_rect = Some(constrain_rect);
286        self
287    }
288
289    /// Where the "root" of the area is.
290    ///
291    /// For instance, if you set this to [`Align2::RIGHT_TOP`]
292    /// then [`Self::fixed_pos`] will set the position of the right-top
293    /// corner of the area.
294    ///
295    /// Default: [`Align2::LEFT_TOP`].
296    #[inline]
297    pub fn pivot(mut self, pivot: Align2) -> Self {
298        self.pivot = pivot;
299        self
300    }
301
302    /// Positions the window but you can still move it.
303    #[inline]
304    pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
305        self.new_pos = Some(current_pos.into());
306        self
307    }
308
309    /// Set anchor and distance.
310    ///
311    /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
312    /// in the right-top corner of the screen".
313    ///
314    /// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
315    /// would move the window left and down from the given anchor.
316    ///
317    /// Anchoring also makes the window immovable.
318    ///
319    /// It is an error to set both an anchor and a position.
320    #[inline]
321    pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
322        self.anchor = Some((align, offset.into()));
323        self.movable(false)
324    }
325
326    pub(crate) fn get_pivot(&self) -> Align2 {
327        if let Some((pivot, _)) = self.anchor {
328            pivot
329        } else {
330            Align2::LEFT_TOP
331        }
332    }
333
334    /// If `true`, quickly fade in the area.
335    ///
336    /// Default: `true`.
337    #[inline]
338    pub fn fade_in(mut self, fade_in: bool) -> Self {
339        self.fade_in = fade_in;
340        self
341    }
342}
343
344pub(crate) struct Prepared {
345    kind: UiKind,
346    layer_id: LayerId,
347    state: AreaState,
348    move_response: Response,
349    enabled: bool,
350    constrain: bool,
351    constrain_rect: Rect,
352
353    /// We always make windows invisible the first frame to hide "first-frame-jitters".
354    ///
355    /// This is so that we use the first frame to calculate the window size,
356    /// and then can correctly position the window and its contents the next frame,
357    /// without having one frame where the window is wrongly positioned or sized.
358    sizing_pass: bool,
359
360    fade_in: bool,
361}
362
363impl Area {
364    pub fn show<R>(
365        self,
366        ctx: &Context,
367        add_contents: impl FnOnce(&mut Ui) -> R,
368    ) -> InnerResponse<R> {
369        let prepared = self.begin(ctx);
370        let mut content_ui = prepared.content_ui(ctx);
371        let inner = add_contents(&mut content_ui);
372        let response = prepared.end(ctx, content_ui);
373        InnerResponse { inner, response }
374    }
375
376    pub(crate) fn begin(self, ctx: &Context) -> Prepared {
377        let Self {
378            id,
379            kind,
380            sense,
381            movable,
382            order,
383            interactable,
384            enabled,
385            default_pos,
386            default_size,
387            new_pos,
388            pivot,
389            anchor,
390            constrain,
391            constrain_rect,
392            fade_in,
393        } = self;
394
395        let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
396
397        let layer_id = LayerId::new(order, id);
398
399        let state = AreaState::load(ctx, id);
400        let mut sizing_pass = state.is_none();
401        let mut state = state.unwrap_or(AreaState {
402            pivot_pos: None,
403            pivot,
404            size: None,
405            interactable,
406            last_became_visible_at: None,
407        });
408        state.pivot = pivot;
409        state.interactable = interactable;
410        if let Some(new_pos) = new_pos {
411            state.pivot_pos = Some(new_pos);
412        }
413        state.pivot_pos.get_or_insert_with(|| {
414            default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id))
415        });
416        state.interactable = interactable;
417
418        let size = *state.size.get_or_insert_with(|| {
419            sizing_pass = true;
420
421            // during the sizing pass we will use this as the max size
422            let mut size = default_size;
423
424            let default_area_size = ctx.style().spacing.default_area_size;
425            if size.x.is_nan() {
426                size.x = default_area_size.x;
427            }
428            if size.y.is_nan() {
429                size.y = default_area_size.y;
430            }
431
432            if constrain {
433                size = size.at_most(constrain_rect.size());
434            }
435
436            size
437        });
438
439        // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
440        let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
441
442        if !visible_last_frame || state.last_became_visible_at.is_none() {
443            state.last_became_visible_at = Some(ctx.input(|i| i.time));
444        }
445
446        if let Some((anchor, offset)) = anchor {
447            state.set_left_top_pos(
448                anchor
449                    .align_size_within_rect(size, constrain_rect)
450                    .left_top()
451                    + offset,
452            );
453        }
454
455        // interact right away to prevent frame-delay
456        let mut move_response = {
457            let interact_id = layer_id.id.with("move");
458            let sense = sense.unwrap_or_else(|| {
459                if movable {
460                    Sense::drag()
461                } else if interactable {
462                    Sense::click() // allow clicks to bring to front
463                } else {
464                    Sense::hover()
465                }
466            });
467
468            let move_response = ctx.create_widget(
469                WidgetRect {
470                    id: interact_id,
471                    layer_id,
472                    rect: state.rect(),
473                    interact_rect: state.rect().intersect(constrain_rect),
474                    sense,
475                    enabled,
476                },
477                true,
478            );
479
480            if movable && move_response.dragged() {
481                if let Some(pivot_pos) = &mut state.pivot_pos {
482                    *pivot_pos += move_response.drag_delta();
483                }
484            }
485
486            if (move_response.dragged() || move_response.clicked())
487                || pointer_pressed_on_area(ctx, layer_id)
488                || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
489            {
490                ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
491                ctx.request_repaint();
492            }
493
494            move_response
495        };
496
497        if constrain {
498            state.set_left_top_pos(
499                Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
500            );
501        }
502
503        state.set_left_top_pos(state.left_top_pos());
504
505        // Update response with possibly moved/constrained rect:
506        move_response.rect = state.rect();
507        move_response.interact_rect = state.rect();
508
509        Prepared {
510            kind,
511            layer_id,
512            state,
513            move_response,
514            enabled,
515            constrain,
516            constrain_rect,
517            sizing_pass,
518            fade_in,
519        }
520    }
521}
522
523impl Prepared {
524    pub(crate) fn state(&self) -> &AreaState {
525        &self.state
526    }
527
528    pub(crate) fn state_mut(&mut self) -> &mut AreaState {
529        &mut self.state
530    }
531
532    pub(crate) fn constrain(&self) -> bool {
533        self.constrain
534    }
535
536    pub(crate) fn constrain_rect(&self) -> Rect {
537        self.constrain_rect
538    }
539
540    pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
541        let max_rect = self.state.rect();
542
543        let mut ui_builder = UiBuilder::new()
544            .ui_stack_info(UiStackInfo::new(self.kind))
545            .layer_id(self.layer_id)
546            .max_rect(max_rect);
547
548        if !self.enabled {
549            ui_builder = ui_builder.disabled();
550        }
551        if self.sizing_pass {
552            ui_builder = ui_builder.sizing_pass().invisible();
553        }
554
555        let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
556        ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
557
558        if self.fade_in {
559            if let Some(last_became_visible_at) = self.state.last_became_visible_at {
560                let age =
561                    ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
562                let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
563                let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
564                ui.multiply_opacity(opacity);
565                if opacity < 1.0 {
566                    ctx.request_repaint();
567                }
568            }
569        }
570
571        ui
572    }
573
574    pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
575        self.move_response.widget_info(make_info);
576    }
577
578    pub(crate) fn id(&self) -> Id {
579        self.move_response.id
580    }
581
582    #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
583    pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
584        let Self {
585            kind: _,
586            layer_id,
587            mut state,
588            move_response: mut response,
589            sizing_pass,
590            ..
591        } = self;
592
593        state.size = Some(content_ui.min_size());
594
595        // Make sure we report back the correct size.
596        // Very important after the initial sizing pass, when the initial estimate of the size is way off.
597        let final_rect = state.rect();
598        response.rect = final_rect;
599        response.interact_rect = final_rect;
600
601        ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
602
603        if sizing_pass {
604            // If we didn't know the size, we were likely drawing the area in the wrong place.
605            ctx.request_repaint();
606        }
607
608        response
609    }
610}
611
612fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
613    if let Some(pointer_pos) = ctx.pointer_interact_pos() {
614        let any_pressed = ctx.input(|i| i.pointer.any_pressed());
615        any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
616    } else {
617        false
618    }
619}
620
621fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
622    let mut existing: Vec<Rect> = ctx.memory(|mem| {
623        mem.areas()
624            .visible_windows()
625            .filter(|(id, _)| id != &layer_id) // ignore ourselves
626            .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some())
627            .map(|(_, state)| state.rect())
628            .collect()
629    });
630    existing.sort_by_key(|r| r.left().round() as i32);
631
632    // NOTE: for the benefit of the egui demo, we position the windows so they don't
633    // cover the side panels, which means we use `available_rect` here instead of `constrain_rect` or `screen_rect`.
634    let available_rect = ctx.available_rect();
635
636    let spacing = 16.0;
637    let left = available_rect.left() + spacing;
638    let top = available_rect.top() + spacing;
639
640    if existing.is_empty() {
641        return pos2(left, top);
642    }
643
644    // Separate existing rectangles into columns:
645    let mut column_bbs = vec![existing[0]];
646
647    for &rect in &existing {
648        let current_column_bb = column_bbs.last_mut().unwrap();
649        if rect.left() < current_column_bb.right() {
650            // same column
651            *current_column_bb = current_column_bb.union(rect);
652        } else {
653            // new column
654            column_bbs.push(rect);
655        }
656    }
657
658    {
659        // Look for large spaces between columns (empty columns):
660        let mut x = left;
661        for col_bb in &column_bbs {
662            let available = col_bb.left() - x;
663            if available >= 300.0 {
664                return pos2(x, top);
665            }
666            x = col_bb.right() + spacing;
667        }
668    }
669
670    // Find first column with some available space at the bottom of it:
671    for col_bb in &column_bbs {
672        if col_bb.bottom() < available_rect.center().y {
673            return pos2(col_bb.left(), col_bb.bottom() + spacing);
674        }
675    }
676
677    // Maybe we can fit a new column?
678    let rightmost = column_bbs.last().unwrap().right();
679    if rightmost + 200.0 < available_rect.right() {
680        return pos2(rightmost + spacing, top);
681    }
682
683    // Ok, just put us in the column with the most space at the bottom:
684    let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
685    for col_bb in &column_bbs {
686        let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
687        if col_pos.y < best_pos.y {
688            best_pos = col_pos;
689        }
690    }
691    best_pos
692}