egui/containers/
frame.rs

1//! Frame container
2
3use crate::{
4    InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
5    layers::ShapeIdx,
6};
7use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
8
9/// A frame around some content, including margin, colors, etc.
10///
11/// ## Definitions
12/// The total (outer) size of a frame is
13/// `content_size + inner_margin + 2 * stroke.width + outer_margin`.
14///
15/// Everything within the stroke is filled with the fill color (if any).
16///
17/// ```text
18/// +-----------------^-------------------------------------- -+
19/// |                 | outer_margin                           |
20/// |    +------------v----^------------------------------+    |
21/// |    |                 | stroke width                 |    |
22/// |    |    +------------v---^---------------------+    |    |
23/// |    |    |                | inner_margin        |    |    |
24/// |    |    |    +-----------v----------------+    |    |    |
25/// |    |    |    |             ^              |    |    |    |
26/// |    |    |    |             |              |    |    |    |
27/// |    |    |    |<------ content_size ------>|    |    |    |
28/// |    |    |    |             |              |    |    |    |
29/// |    |    |    |             v              |    |    |    |
30/// |    |    |    +------- content_rect -------+    |    |    |
31/// |    |    |                                      |    |    |
32/// |    |    +-------------fill_rect ---------------+    |    |
33/// |    |                                                |    |
34/// |    +----------------- widget_rect ------------------+    |
35/// |                                                          |
36/// +---------------------- outer_rect ------------------------+
37/// ```
38///
39/// The four rectangles, from inside to outside, are:
40/// * `content_rect`: the rectangle that is made available to the inner [`Ui`] or widget.
41/// * `fill_rect`: the rectangle that is filled with the fill color (inside the stroke, if any).
42/// * `widget_rect`: is the interactive part of the widget (what sense clicks etc).
43/// * `outer_rect`: what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
44///
45/// ## Usage
46///
47/// ```
48/// # egui::__run_test_ui(|ui| {
49/// egui::Frame::none()
50///     .fill(egui::Color32::RED)
51///     .show(ui, |ui| {
52///         ui.label("Label with red background");
53///     });
54/// # });
55/// ```
56///
57/// ## Dynamic color
58/// If you want to change the color of the frame based on the response of
59/// the widget, you need to break it up into multiple steps:
60///
61/// ```
62/// # egui::__run_test_ui(|ui| {
63/// let mut frame = egui::Frame::default().inner_margin(4.0).begin(ui);
64/// {
65///     let response = frame.content_ui.label("Inside the frame");
66///     if response.hovered() {
67///         frame.frame.fill = egui::Color32::RED;
68///     }
69/// }
70/// frame.end(ui); // Will "close" the frame.
71/// # });
72/// ```
73///
74/// You can also respond to the hovering of the frame itself:
75///
76/// ```
77/// # egui::__run_test_ui(|ui| {
78/// let mut frame = egui::Frame::default().inner_margin(4.0).begin(ui);
79/// {
80///     frame.content_ui.label("Inside the frame");
81///     frame.content_ui.label("This too");
82/// }
83/// let response = frame.allocate_space(ui);
84/// if response.hovered() {
85///     frame.frame.fill = egui::Color32::RED;
86/// }
87/// frame.paint(ui);
88/// # });
89/// ```
90///
91/// Note that you cannot change the margins after calling `begin`.
92#[doc(alias = "border")]
93#[derive(Clone, Copy, Debug, Default, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
95#[must_use = "You should call .show()"]
96pub struct Frame {
97    // Fields are ordered inside-out.
98    // TODO(emilk): add `min_content_size: Vec2`
99    //
100    /// Margin within the painted frame.
101    ///
102    /// Known as `padding` in CSS.
103    #[doc(alias = "padding")]
104    pub inner_margin: Margin,
105
106    /// The background fill color of the frame, within the [`Self::stroke`].
107    ///
108    /// Known as `background` in CSS.
109    #[doc(alias = "background")]
110    pub fill: Color32,
111
112    /// The width and color of the outline around the frame.
113    ///
114    /// The width of the stroke is part of the total margin/padding of the frame.
115    #[doc(alias = "border")]
116    pub stroke: Stroke,
117
118    /// The rounding of the _outer_ corner of the [`Self::stroke`]
119    /// (or, if there is no stroke, the outer corner of [`Self::fill`]).
120    ///
121    /// In other words, this is the corner radius of the _widget rect_.
122    pub corner_radius: CornerRadius,
123
124    /// Margin outside the painted frame.
125    ///
126    /// Similar to what is called `margin` in CSS.
127    /// However, egui does NOT do "Margin Collapse" like in CSS,
128    /// i.e. when placing two frames next to each other,
129    /// the distance between their borders is the SUM
130    /// of their other margins.
131    /// In CSS the distance would be the MAX of their outer margins.
132    /// Supporting margin collapse is difficult, and would
133    /// requires complicating the already complicated egui layout code.
134    ///
135    /// Consider using [`crate::Spacing::item_spacing`]
136    /// for adding space between widgets.
137    pub outer_margin: Margin,
138
139    /// Optional drop-shadow behind the frame.
140    pub shadow: Shadow,
141}
142
143#[test]
144fn frame_size() {
145    assert_eq!(
146        std::mem::size_of::<Frame>(),
147        32,
148        "Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
149    );
150    assert!(
151        std::mem::size_of::<Frame>() <= 64,
152        "Frame is getting way too big!"
153    );
154}
155
156/// ## Constructors
157impl Frame {
158    /// No colors, no margins, no border.
159    ///
160    /// This is also the default.
161    pub const NONE: Self = Self {
162        inner_margin: Margin::ZERO,
163        stroke: Stroke::NONE,
164        fill: Color32::TRANSPARENT,
165        corner_radius: CornerRadius::ZERO,
166        outer_margin: Margin::ZERO,
167        shadow: Shadow::NONE,
168    };
169
170    /// No colors, no margins, no border.
171    ///
172    /// Same as [`Frame::NONE`].
173    pub const fn new() -> Self {
174        Self::NONE
175    }
176
177    #[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."]
178    pub const fn none() -> Self {
179        Self::NONE
180    }
181
182    /// For when you want to group a few widgets together within a frame.
183    pub fn group(style: &Style) -> Self {
184        Self::new()
185            .inner_margin(6)
186            .corner_radius(style.visuals.widgets.noninteractive.corner_radius)
187            .stroke(style.visuals.widgets.noninteractive.bg_stroke)
188    }
189
190    pub fn side_top_panel(style: &Style) -> Self {
191        Self::new()
192            .inner_margin(Margin::symmetric(8, 2))
193            .fill(style.visuals.panel_fill)
194    }
195
196    pub fn central_panel(style: &Style) -> Self {
197        Self::new().inner_margin(8).fill(style.visuals.panel_fill)
198    }
199
200    pub fn window(style: &Style) -> Self {
201        Self::new()
202            .inner_margin(style.spacing.window_margin)
203            .corner_radius(style.visuals.window_corner_radius)
204            .shadow(style.visuals.window_shadow)
205            .fill(style.visuals.window_fill())
206            .stroke(style.visuals.window_stroke())
207    }
208
209    pub fn menu(style: &Style) -> Self {
210        Self::new()
211            .inner_margin(style.spacing.menu_margin)
212            .corner_radius(style.visuals.menu_corner_radius)
213            .shadow(style.visuals.popup_shadow)
214            .fill(style.visuals.window_fill())
215            .stroke(style.visuals.window_stroke())
216    }
217
218    pub fn popup(style: &Style) -> Self {
219        Self::new()
220            .inner_margin(style.spacing.menu_margin)
221            .corner_radius(style.visuals.menu_corner_radius)
222            .shadow(style.visuals.popup_shadow)
223            .fill(style.visuals.window_fill())
224            .stroke(style.visuals.window_stroke())
225    }
226
227    /// A canvas to draw on.
228    ///
229    /// In bright mode this will be very bright,
230    /// and in dark mode this will be very dark.
231    pub fn canvas(style: &Style) -> Self {
232        Self::new()
233            .inner_margin(2)
234            .corner_radius(style.visuals.widgets.noninteractive.corner_radius)
235            .fill(style.visuals.extreme_bg_color)
236            .stroke(style.visuals.window_stroke())
237    }
238
239    /// A dark canvas to draw on.
240    pub fn dark_canvas(style: &Style) -> Self {
241        Self::canvas(style).fill(Color32::from_black_alpha(250))
242    }
243}
244
245/// ## Builders
246impl Frame {
247    /// Margin within the painted frame.
248    ///
249    /// Known as `padding` in CSS.
250    #[doc(alias = "padding")]
251    #[inline]
252    pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
253        self.inner_margin = inner_margin.into();
254        self
255    }
256
257    /// The background fill color of the frame, within the [`Self::stroke`].
258    ///
259    /// Known as `background` in CSS.
260    #[doc(alias = "background")]
261    #[inline]
262    pub fn fill(mut self, fill: Color32) -> Self {
263        self.fill = fill;
264        self
265    }
266
267    /// The width and color of the outline around the frame.
268    ///
269    /// The width of the stroke is part of the total margin/padding of the frame.
270    #[inline]
271    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
272        self.stroke = stroke.into();
273        self
274    }
275
276    /// The rounding of the _outer_ corner of the [`Self::stroke`]
277    /// (or, if there is no stroke, the outer corner of [`Self::fill`]).
278    ///
279    /// In other words, this is the corner radius of the _widget rect_.
280    #[inline]
281    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
282        self.corner_radius = corner_radius.into();
283        self
284    }
285
286    /// The rounding of the _outer_ corner of the [`Self::stroke`]
287    /// (or, if there is no stroke, the outer corner of [`Self::fill`]).
288    ///
289    /// In other words, this is the corner radius of the _widget rect_.
290    #[inline]
291    #[deprecated = "Renamed to `corner_radius`"]
292    pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
293        self.corner_radius(corner_radius)
294    }
295
296    /// Margin outside the painted frame.
297    ///
298    /// Similar to what is called `margin` in CSS.
299    /// However, egui does NOT do "Margin Collapse" like in CSS,
300    /// i.e. when placing two frames next to each other,
301    /// the distance between their borders is the SUM
302    /// of their other margins.
303    /// In CSS the distance would be the MAX of their outer margins.
304    /// Supporting margin collapse is difficult, and would
305    /// requires complicating the already complicated egui layout code.
306    ///
307    /// Consider using [`crate::Spacing::item_spacing`]
308    /// for adding space between widgets.
309    #[inline]
310    pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
311        self.outer_margin = outer_margin.into();
312        self
313    }
314
315    /// Optional drop-shadow behind the frame.
316    #[inline]
317    pub fn shadow(mut self, shadow: Shadow) -> Self {
318        self.shadow = shadow;
319        self
320    }
321
322    /// Opacity multiplier in gamma space.
323    ///
324    /// For instance, multiplying with `0.5`
325    /// will make the frame half transparent.
326    #[inline]
327    pub fn multiply_with_opacity(mut self, opacity: f32) -> Self {
328        self.fill = self.fill.gamma_multiply(opacity);
329        self.stroke.color = self.stroke.color.gamma_multiply(opacity);
330        self.shadow.color = self.shadow.color.gamma_multiply(opacity);
331        self
332    }
333}
334
335/// ## Inspectors
336impl Frame {
337    /// How much extra space the frame uses up compared to the content.
338    ///
339    /// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
340    #[inline]
341    pub fn total_margin(&self) -> MarginF32 {
342        MarginF32::from(self.inner_margin)
343            + MarginF32::from(self.stroke.width)
344            + MarginF32::from(self.outer_margin)
345    }
346
347    /// Calculate the `fill_rect` from the `content_rect`.
348    ///
349    /// This is the rectangle that is filled with the fill color (inside the stroke, if any).
350    pub fn fill_rect(&self, content_rect: Rect) -> Rect {
351        content_rect + self.inner_margin
352    }
353
354    /// Calculate the `widget_rect` from the `content_rect`.
355    ///
356    /// This is the visible and interactive rectangle.
357    pub fn widget_rect(&self, content_rect: Rect) -> Rect {
358        content_rect + self.inner_margin + MarginF32::from(self.stroke.width)
359    }
360
361    /// Calculate the `outer_rect` from the `content_rect`.
362    ///
363    /// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
364    pub fn outer_rect(&self, content_rect: Rect) -> Rect {
365        content_rect + self.inner_margin + MarginF32::from(self.stroke.width) + self.outer_margin
366    }
367}
368
369// ----------------------------------------------------------------------------
370
371pub struct Prepared {
372    /// The frame that was prepared.
373    ///
374    /// The margin has already been read and used,
375    /// but the rest of the fields may be modified.
376    pub frame: Frame,
377
378    /// This is where we will insert the frame shape so it ends up behind the content.
379    where_to_put_background: ShapeIdx,
380
381    /// Add your widgets to this UI so it ends up within the frame.
382    pub content_ui: Ui,
383}
384
385impl Frame {
386    /// Begin a dynamically colored frame.
387    ///
388    /// This is a more advanced API.
389    /// Usually you want to use [`Self::show`] instead.
390    ///
391    /// See docs for [`Frame`] for an example.
392    pub fn begin(self, ui: &mut Ui) -> Prepared {
393        let where_to_put_background = ui.painter().add(Shape::Noop);
394        let outer_rect_bounds = ui.available_rect_before_wrap();
395
396        let mut max_content_rect = outer_rect_bounds - self.total_margin();
397
398        // Make sure we don't shrink to the negative:
399        max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x);
400        max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y);
401
402        let content_ui = ui.new_child(
403            UiBuilder::new()
404                .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self))
405                .max_rect(max_content_rect),
406        );
407
408        Prepared {
409            frame: self,
410            where_to_put_background,
411            content_ui,
412        }
413    }
414
415    /// Show the given ui surrounded by this frame.
416    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
417        self.show_dyn(ui, Box::new(add_contents))
418    }
419
420    /// Show using dynamic dispatch.
421    pub fn show_dyn<'c, R>(
422        self,
423        ui: &mut Ui,
424        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
425    ) -> InnerResponse<R> {
426        let mut prepared = self.begin(ui);
427        let ret = add_contents(&mut prepared.content_ui);
428        let response = prepared.end(ui);
429        InnerResponse::new(ret, response)
430    }
431
432    /// Paint this frame as a shape.
433    pub fn paint(&self, content_rect: Rect) -> Shape {
434        let Self {
435            inner_margin: _,
436            fill,
437            stroke,
438            corner_radius,
439            outer_margin: _,
440            shadow,
441        } = *self;
442
443        let widget_rect = self.widget_rect(content_rect);
444
445        let frame_shape = Shape::Rect(epaint::RectShape::new(
446            widget_rect,
447            corner_radius,
448            fill,
449            stroke,
450            epaint::StrokeKind::Inside,
451        ));
452
453        if shadow == Default::default() {
454            frame_shape
455        } else {
456            let shadow = shadow.as_shape(widget_rect, corner_radius);
457            Shape::Vec(vec![Shape::from(shadow), frame_shape])
458        }
459    }
460}
461
462impl Prepared {
463    fn outer_rect(&self) -> Rect {
464        let content_rect = self.content_ui.min_rect();
465        content_rect
466            + self.frame.inner_margin
467            + MarginF32::from(self.frame.stroke.width)
468            + self.frame.outer_margin
469    }
470
471    /// Allocate the space that was used by [`Self::content_ui`].
472    ///
473    /// This MUST be called, or the parent ui will not know how much space this widget used.
474    ///
475    /// This can be called before or after [`Self::paint`].
476    pub fn allocate_space(&self, ui: &mut Ui) -> Response {
477        ui.allocate_rect(self.outer_rect(), Sense::hover())
478    }
479
480    /// Paint the frame.
481    ///
482    /// This can be called before or after [`Self::allocate_space`].
483    pub fn paint(&self, ui: &Ui) {
484        let content_rect = self.content_ui.min_rect();
485        let widget_rect = self.frame.widget_rect(content_rect);
486
487        if ui.is_rect_visible(widget_rect) {
488            let shape = self.frame.paint(content_rect);
489            ui.painter().set(self.where_to_put_background, shape);
490        }
491    }
492
493    /// Convenience for calling [`Self::allocate_space`] and [`Self::paint`].
494    ///
495    /// Returns the outer rect, i.e. including the outer margin.
496    pub fn end(self, ui: &mut Ui) -> Response {
497        self.paint(ui);
498        self.allocate_space(ui)
499    }
500}