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