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    Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
9    Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
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, Debug)]
107pub struct Area {
108    pub(crate) id: Id,
109    info: UiStackInfo,
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    layout: Layout,
124    sizing_pass: bool,
125}
126
127impl WidgetWithState for Area {
128    type State = AreaState;
129}
130
131impl Area {
132    /// The `id` must be globally unique.
133    pub fn new(id: Id) -> Self {
134        Self {
135            id,
136            info: UiStackInfo::new(UiKind::GenericArea),
137            sense: None,
138            movable: true,
139            interactable: true,
140            constrain: true,
141            constrain_rect: None,
142            enabled: true,
143            order: Order::Middle,
144            default_pos: None,
145            default_size: Vec2::NAN,
146            new_pos: None,
147            pivot: Align2::LEFT_TOP,
148            anchor: None,
149            fade_in: true,
150            layout: Layout::default(),
151            sizing_pass: false,
152        }
153    }
154
155    /// Let's you change the `id` that you assigned in [`Self::new`].
156    ///
157    /// The `id` must be globally unique.
158    #[inline]
159    pub fn id(mut self, id: Id) -> Self {
160        self.id = id;
161        self
162    }
163
164    /// Change the [`UiKind`] of the arena.
165    ///
166    /// Default to [`UiKind::GenericArea`].
167    #[inline]
168    pub fn kind(mut self, kind: UiKind) -> Self {
169        self.info = UiStackInfo::new(kind);
170        self
171    }
172
173    /// Set the [`UiStackInfo`] of the area's [`Ui`].
174    ///
175    /// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
176    #[inline]
177    pub fn info(mut self, info: UiStackInfo) -> Self {
178        self.info = info;
179        self
180    }
181
182    pub fn layer(&self) -> LayerId {
183        LayerId::new(self.order, self.id)
184    }
185
186    /// If false, no content responds to click
187    /// and widgets will be shown grayed out.
188    /// You won't be able to move the window.
189    /// Default: `true`.
190    #[inline]
191    pub fn enabled(mut self, enabled: bool) -> Self {
192        self.enabled = enabled;
193        self
194    }
195
196    /// Moveable by dragging the area?
197    #[inline]
198    pub fn movable(mut self, movable: bool) -> Self {
199        self.movable = movable;
200        self.interactable |= movable;
201        self
202    }
203
204    pub fn is_enabled(&self) -> bool {
205        self.enabled
206    }
207
208    pub fn is_movable(&self) -> bool {
209        self.movable && self.enabled
210    }
211
212    /// If false, clicks goes straight through to what is behind us.
213    ///
214    /// Can be used for semi-invisible areas that the user should be able to click through.
215    ///
216    /// Default: `true`.
217    #[inline]
218    pub fn interactable(mut self, interactable: bool) -> Self {
219        self.interactable = interactable;
220        self.movable &= interactable;
221        self
222    }
223
224    /// Explicitly set a sense.
225    ///
226    /// If not set, this will default to `Sense::drag()` if movable, `Sense::click()` if interactable, and `Sense::hover()` otherwise.
227    #[inline]
228    pub fn sense(mut self, sense: Sense) -> Self {
229        self.sense = Some(sense);
230        self
231    }
232
233    /// `order(Order::Foreground)` for an Area that should always be on top
234    #[inline]
235    pub fn order(mut self, order: Order) -> Self {
236        self.order = order;
237        self
238    }
239
240    #[inline]
241    pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
242        self.default_pos = Some(default_pos.into());
243        self
244    }
245
246    /// The size used for the [`Ui::max_rect`] the first frame.
247    ///
248    /// Text will wrap at this width, and images that expand to fill the available space
249    /// will expand to this size.
250    ///
251    /// If the contents are smaller than this size, the area will shrink to fit the contents.
252    /// If the contents overflow, the area will grow.
253    ///
254    /// If not set, [`crate::style::Spacing::default_area_size`] will be used.
255    #[inline]
256    pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
257        self.default_size = default_size.into();
258        self
259    }
260
261    /// See [`Self::default_size`].
262    #[inline]
263    pub fn default_width(mut self, default_width: f32) -> Self {
264        self.default_size.x = default_width;
265        self
266    }
267
268    /// See [`Self::default_size`].
269    #[inline]
270    pub fn default_height(mut self, default_height: f32) -> Self {
271        self.default_size.y = default_height;
272        self
273    }
274
275    /// Positions the window and prevents it from being moved
276    #[inline]
277    pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
278        self.new_pos = Some(fixed_pos.into());
279        self.movable = false;
280        self
281    }
282
283    /// Constrains this area to [`Context::screen_rect`]?
284    ///
285    /// Default: `true`.
286    #[inline]
287    pub fn constrain(mut self, constrain: bool) -> Self {
288        self.constrain = constrain;
289        self
290    }
291
292    /// Constrain the movement of the window to the given rectangle.
293    ///
294    /// For instance: `.constrain_to(ctx.screen_rect())`.
295    #[inline]
296    pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
297        self.constrain = true;
298        self.constrain_rect = Some(constrain_rect);
299        self
300    }
301
302    /// Where the "root" of the area is.
303    ///
304    /// For instance, if you set this to [`Align2::RIGHT_TOP`]
305    /// then [`Self::fixed_pos`] will set the position of the right-top
306    /// corner of the area.
307    ///
308    /// Default: [`Align2::LEFT_TOP`].
309    #[inline]
310    pub fn pivot(mut self, pivot: Align2) -> Self {
311        self.pivot = pivot;
312        self
313    }
314
315    /// Positions the window but you can still move it.
316    #[inline]
317    pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
318        self.new_pos = Some(current_pos.into());
319        self
320    }
321
322    /// Set anchor and distance.
323    ///
324    /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
325    /// in the right-top corner of the screen".
326    ///
327    /// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
328    /// would move the window left and down from the given anchor.
329    ///
330    /// Anchoring also makes the window immovable.
331    ///
332    /// It is an error to set both an anchor and a position.
333    #[inline]
334    pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
335        self.anchor = Some((align, offset.into()));
336        self.movable(false)
337    }
338
339    pub(crate) fn get_pivot(&self) -> Align2 {
340        if let Some((pivot, _)) = self.anchor {
341            pivot
342        } else {
343            Align2::LEFT_TOP
344        }
345    }
346
347    /// If `true`, quickly fade in the area.
348    ///
349    /// Default: `true`.
350    #[inline]
351    pub fn fade_in(mut self, fade_in: bool) -> Self {
352        self.fade_in = fade_in;
353        self
354    }
355
356    /// Set the layout for the child Ui.
357    #[inline]
358    pub fn layout(mut self, layout: Layout) -> Self {
359        self.layout = layout;
360        self
361    }
362
363    /// While true, a sizing pass will be done. This means the area will be invisible
364    /// and the contents will be laid out to estimate the proper containing size of the area.
365    /// If false, there will be no change to the default area behavior. This is useful if the
366    /// area contents area dynamic and you need to need to make sure the area adjusts its size
367    /// accordingly.
368    ///
369    /// This should only be set to true during the specific frames you want force a sizing pass.
370    /// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
371    /// visible.
372    ///
373    /// # Arguments
374    /// - resize: If true, the area will be resized to fit its contents. False will keep the
375    ///   default area resizing behavior.
376    ///
377    /// Default: `false`.
378    #[inline]
379    pub fn sizing_pass(mut self, resize: bool) -> Self {
380        self.sizing_pass = resize;
381        self
382    }
383}
384
385pub(crate) struct Prepared {
386    info: Option<UiStackInfo>,
387    layer_id: LayerId,
388    state: AreaState,
389    move_response: Response,
390    enabled: bool,
391    constrain: bool,
392    constrain_rect: Rect,
393
394    /// We always make windows invisible the first frame to hide "first-frame-jitters".
395    ///
396    /// This is so that we use the first frame to calculate the window size,
397    /// and then can correctly position the window and its contents the next frame,
398    /// without having one frame where the window is wrongly positioned or sized.
399    sizing_pass: bool,
400
401    fade_in: bool,
402    layout: Layout,
403}
404
405impl Area {
406    pub fn show<R>(
407        self,
408        ctx: &Context,
409        add_contents: impl FnOnce(&mut Ui) -> R,
410    ) -> InnerResponse<R> {
411        let mut prepared = self.begin(ctx);
412        let mut content_ui = prepared.content_ui(ctx);
413        let inner = add_contents(&mut content_ui);
414        let response = prepared.end(ctx, content_ui);
415        InnerResponse { inner, response }
416    }
417
418    pub(crate) fn begin(self, ctx: &Context) -> Prepared {
419        let Self {
420            id,
421            info,
422            sense,
423            movable,
424            order,
425            interactable,
426            enabled,
427            default_pos,
428            default_size,
429            new_pos,
430            pivot,
431            anchor,
432            constrain,
433            constrain_rect,
434            fade_in,
435            layout,
436            sizing_pass: force_sizing_pass,
437        } = self;
438
439        let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
440
441        let layer_id = LayerId::new(order, id);
442
443        let state = AreaState::load(ctx, id);
444        let mut sizing_pass = state.is_none();
445        let mut state = state.unwrap_or(AreaState {
446            pivot_pos: None,
447            pivot,
448            size: None,
449            interactable,
450            last_became_visible_at: None,
451        });
452        if force_sizing_pass {
453            sizing_pass = true;
454            state.size = None;
455        }
456        state.pivot = pivot;
457        state.interactable = interactable;
458        if let Some(new_pos) = new_pos {
459            state.pivot_pos = Some(new_pos);
460        }
461        state.pivot_pos.get_or_insert_with(|| {
462            default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id))
463        });
464        state.interactable = interactable;
465
466        let size = *state.size.get_or_insert_with(|| {
467            sizing_pass = true;
468
469            // during the sizing pass we will use this as the max size
470            let mut size = default_size;
471
472            let default_area_size = ctx.style().spacing.default_area_size;
473            if size.x.is_nan() {
474                size.x = default_area_size.x;
475            }
476            if size.y.is_nan() {
477                size.y = default_area_size.y;
478            }
479
480            if constrain {
481                size = size.at_most(constrain_rect.size());
482            }
483
484            size
485        });
486
487        // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
488        let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
489
490        if !visible_last_frame || state.last_became_visible_at.is_none() {
491            state.last_became_visible_at = Some(ctx.input(|i| i.time));
492        }
493
494        if let Some((anchor, offset)) = anchor {
495            state.set_left_top_pos(
496                anchor
497                    .align_size_within_rect(size, constrain_rect)
498                    .left_top()
499                    + offset,
500            );
501        }
502
503        // interact right away to prevent frame-delay
504        let mut move_response = {
505            let interact_id = layer_id.id.with("move");
506            let sense = sense.unwrap_or_else(|| {
507                if movable {
508                    Sense::drag()
509                } else if interactable {
510                    Sense::click() // allow clicks to bring to front
511                } else {
512                    Sense::hover()
513                }
514            });
515
516            let move_response = ctx.create_widget(
517                WidgetRect {
518                    id: interact_id,
519                    layer_id,
520                    rect: state.rect(),
521                    interact_rect: state.rect().intersect(constrain_rect),
522                    sense,
523                    enabled,
524                },
525                true,
526            );
527
528            if movable && move_response.dragged() {
529                if let Some(pivot_pos) = &mut state.pivot_pos {
530                    *pivot_pos += move_response.drag_delta();
531                }
532            }
533
534            if (move_response.dragged() || move_response.clicked())
535                || pointer_pressed_on_area(ctx, layer_id)
536                || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
537            {
538                ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
539                ctx.request_repaint();
540            }
541
542            move_response
543        };
544
545        if constrain {
546            state.set_left_top_pos(
547                Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
548            );
549        }
550
551        state.set_left_top_pos(state.left_top_pos());
552
553        // Update response with possibly moved/constrained rect:
554        move_response.rect = state.rect();
555        move_response.interact_rect = state.rect();
556
557        Prepared {
558            info: Some(info),
559            layer_id,
560            state,
561            move_response,
562            enabled,
563            constrain,
564            constrain_rect,
565            sizing_pass,
566            fade_in,
567            layout,
568        }
569    }
570}
571
572impl Prepared {
573    pub(crate) fn state(&self) -> &AreaState {
574        &self.state
575    }
576
577    pub(crate) fn state_mut(&mut self) -> &mut AreaState {
578        &mut self.state
579    }
580
581    pub(crate) fn constrain(&self) -> bool {
582        self.constrain
583    }
584
585    pub(crate) fn constrain_rect(&self) -> Rect {
586        self.constrain_rect
587    }
588
589    pub(crate) fn content_ui(&mut self, ctx: &Context) -> Ui {
590        let max_rect = self.state.rect();
591
592        let mut ui_builder = UiBuilder::new()
593            .ui_stack_info(self.info.take().unwrap_or_default())
594            .layer_id(self.layer_id)
595            .max_rect(max_rect)
596            .layout(self.layout)
597            .closable();
598
599        if !self.enabled {
600            ui_builder = ui_builder.disabled();
601        }
602        if self.sizing_pass {
603            ui_builder = ui_builder.sizing_pass().invisible();
604        }
605
606        let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
607        ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
608
609        if self.fade_in {
610            if let Some(last_became_visible_at) = self.state.last_became_visible_at {
611                let age =
612                    ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
613                let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
614                let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
615                ui.multiply_opacity(opacity);
616                if opacity < 1.0 {
617                    ctx.request_repaint();
618                }
619            }
620        }
621
622        ui
623    }
624
625    pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
626        self.move_response.widget_info(make_info);
627    }
628
629    pub(crate) fn id(&self) -> Id {
630        self.move_response.id
631    }
632
633    #[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
634    pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
635        let Self {
636            info: _,
637            layer_id,
638            mut state,
639            move_response: mut response,
640            sizing_pass,
641            ..
642        } = self;
643
644        state.size = Some(content_ui.min_size());
645
646        // Make sure we report back the correct size.
647        // Very important after the initial sizing pass, when the initial estimate of the size is way off.
648        let final_rect = state.rect();
649        response.rect = final_rect;
650        response.interact_rect = final_rect;
651
652        // TODO(lucasmerlin): Can the area response be based on Ui::response? Then this won't be needed
653        // Bubble up the close event
654        if content_ui.should_close() {
655            response.set_close();
656        }
657
658        ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
659
660        if sizing_pass {
661            // If we didn't know the size, we were likely drawing the area in the wrong place.
662            ctx.request_repaint();
663        }
664
665        response
666    }
667}
668
669fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
670    if let Some(pointer_pos) = ctx.pointer_interact_pos() {
671        let any_pressed = ctx.input(|i| i.pointer.any_pressed());
672        any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
673    } else {
674        false
675    }
676}
677
678fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
679    let mut existing: Vec<Rect> = ctx.memory(|mem| {
680        mem.areas()
681            .visible_windows()
682            .filter(|(id, _)| id != &layer_id) // ignore ourselves
683            .filter(|(_, state)| state.pivot_pos.is_some() && state.size.is_some())
684            .map(|(_, state)| state.rect())
685            .collect()
686    });
687    existing.sort_by_key(|r| r.left().round() as i32);
688
689    // NOTE: for the benefit of the egui demo, we position the windows so they don't
690    // cover the side panels, which means we use `available_rect` here instead of `constrain_rect` or `screen_rect`.
691    let available_rect = ctx.available_rect();
692
693    let spacing = 16.0;
694    let left = available_rect.left() + spacing;
695    let top = available_rect.top() + spacing;
696
697    if existing.is_empty() {
698        return pos2(left, top);
699    }
700
701    // Separate existing rectangles into columns:
702    let mut column_bbs = vec![existing[0]];
703
704    for &rect in &existing {
705        let current_column_bb = column_bbs.last_mut().unwrap();
706        if rect.left() < current_column_bb.right() {
707            // same column
708            *current_column_bb |= rect;
709        } else {
710            // new column
711            column_bbs.push(rect);
712        }
713    }
714
715    {
716        // Look for large spaces between columns (empty columns):
717        let mut x = left;
718        for col_bb in &column_bbs {
719            let available = col_bb.left() - x;
720            if available >= 300.0 {
721                return pos2(x, top);
722            }
723            x = col_bb.right() + spacing;
724        }
725    }
726
727    // Find first column with some available space at the bottom of it:
728    for col_bb in &column_bbs {
729        if col_bb.bottom() < available_rect.center().y {
730            return pos2(col_bb.left(), col_bb.bottom() + spacing);
731        }
732    }
733
734    // Maybe we can fit a new column?
735    let rightmost = column_bbs.last().unwrap().right();
736    if rightmost + 200.0 < available_rect.right() {
737        return pos2(rightmost + spacing, top);
738    }
739
740    // Ok, just put us in the column with the most space at the bottom:
741    let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
742    for col_bb in &column_bbs {
743        let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
744        if col_pos.y < best_pos.y {
745            best_pos = col_pos;
746        }
747    }
748    best_pos
749}