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