egui/containers/frame.rs
1//! Frame container
2
3use crate::{
4 epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
5 UiStackInfo,
6};
7use epaint::{Color32, CornerRadius, Margin, Marginf, 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>(), 32,
147 "Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
148 );
149 assert!(
150 std::mem::size_of::<Frame>() <= 64,
151 "Frame is getting way too big!"
152 );
153}
154
155/// ## Constructors
156impl Frame {
157 /// No colors, no margins, no border.
158 ///
159 /// This is also the default.
160 pub const NONE: Self = Self {
161 inner_margin: Margin::ZERO,
162 stroke: Stroke::NONE,
163 fill: Color32::TRANSPARENT,
164 corner_radius: CornerRadius::ZERO,
165 outer_margin: Margin::ZERO,
166 shadow: Shadow::NONE,
167 };
168
169 /// No colors, no margins, no border.
170 ///
171 /// Same as [`Frame::NONE`].
172 pub const fn new() -> Self {
173 Self::NONE
174 }
175
176 #[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."]
177 pub const fn none() -> Self {
178 Self::NONE
179 }
180
181 /// For when you want to group a few widgets together within a frame.
182 pub fn group(style: &Style) -> Self {
183 Self::new()
184 .inner_margin(6)
185 .corner_radius(style.visuals.widgets.noninteractive.corner_radius)
186 .stroke(style.visuals.widgets.noninteractive.bg_stroke)
187 }
188
189 pub fn side_top_panel(style: &Style) -> Self {
190 Self::new()
191 .inner_margin(Margin::symmetric(8, 2))
192 .fill(style.visuals.panel_fill)
193 }
194
195 pub fn central_panel(style: &Style) -> Self {
196 Self::new().inner_margin(8).fill(style.visuals.panel_fill)
197 }
198
199 pub fn window(style: &Style) -> Self {
200 Self::new()
201 .inner_margin(style.spacing.window_margin)
202 .corner_radius(style.visuals.window_corner_radius)
203 .shadow(style.visuals.window_shadow)
204 .fill(style.visuals.window_fill())
205 .stroke(style.visuals.window_stroke())
206 }
207
208 pub fn menu(style: &Style) -> Self {
209 Self::new()
210 .inner_margin(style.spacing.menu_margin)
211 .corner_radius(style.visuals.menu_corner_radius)
212 .shadow(style.visuals.popup_shadow)
213 .fill(style.visuals.window_fill())
214 .stroke(style.visuals.window_stroke())
215 }
216
217 pub fn popup(style: &Style) -> Self {
218 Self::new()
219 .inner_margin(style.spacing.menu_margin)
220 .corner_radius(style.visuals.menu_corner_radius)
221 .shadow(style.visuals.popup_shadow)
222 .fill(style.visuals.window_fill())
223 .stroke(style.visuals.window_stroke())
224 }
225
226 /// A canvas to draw on.
227 ///
228 /// In bright mode this will be very bright,
229 /// and in dark mode this will be very dark.
230 pub fn canvas(style: &Style) -> Self {
231 Self::new()
232 .inner_margin(2)
233 .corner_radius(style.visuals.widgets.noninteractive.corner_radius)
234 .fill(style.visuals.extreme_bg_color)
235 .stroke(style.visuals.window_stroke())
236 }
237
238 /// A dark canvas to draw on.
239 pub fn dark_canvas(style: &Style) -> Self {
240 Self::canvas(style).fill(Color32::from_black_alpha(250))
241 }
242}
243
244/// ## Builders
245impl Frame {
246 /// Margin within the painted frame.
247 ///
248 /// Known as `padding` in CSS.
249 #[doc(alias = "padding")]
250 #[inline]
251 pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
252 self.inner_margin = inner_margin.into();
253 self
254 }
255
256 /// The background fill color of the frame, within the [`Self::stroke`].
257 ///
258 /// Known as `background` in CSS.
259 #[doc(alias = "background")]
260 #[inline]
261 pub fn fill(mut self, fill: Color32) -> Self {
262 self.fill = fill;
263 self
264 }
265
266 /// The width and color of the outline around the frame.
267 ///
268 /// The width of the stroke is part of the total margin/padding of the frame.
269 #[inline]
270 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
271 self.stroke = stroke.into();
272 self
273 }
274
275 /// The rounding of the _outer_ corner of the [`Self::stroke`]
276 /// (or, if there is no stroke, the outer corner of [`Self::fill`]).
277 ///
278 /// In other words, this is the corner radius of the _widget rect_.
279 #[inline]
280 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
281 self.corner_radius = corner_radius.into();
282 self
283 }
284
285 /// The rounding of the _outer_ corner of the [`Self::stroke`]
286 /// (or, if there is no stroke, the outer corner of [`Self::fill`]).
287 ///
288 /// In other words, this is the corner radius of the _widget rect_.
289 #[inline]
290 #[deprecated = "Renamed to `corner_radius`"]
291 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
292 self.corner_radius(corner_radius)
293 }
294
295 /// Margin outside the painted frame.
296 ///
297 /// Similar to what is called `margin` in CSS.
298 /// However, egui does NOT do "Margin Collapse" like in CSS,
299 /// i.e. when placing two frames next to each other,
300 /// the distance between their borders is the SUM
301 /// of their other margins.
302 /// In CSS the distance would be the MAX of their outer margins.
303 /// Supporting margin collapse is difficult, and would
304 /// requires complicating the already complicated egui layout code.
305 ///
306 /// Consider using [`crate::Spacing::item_spacing`]
307 /// for adding space between widgets.
308 #[inline]
309 pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
310 self.outer_margin = outer_margin.into();
311 self
312 }
313
314 /// Optional drop-shadow behind the frame.
315 #[inline]
316 pub fn shadow(mut self, shadow: Shadow) -> Self {
317 self.shadow = shadow;
318 self
319 }
320
321 /// Opacity multiplier in gamma space.
322 ///
323 /// For instance, multiplying with `0.5`
324 /// will make the frame half transparent.
325 #[inline]
326 pub fn multiply_with_opacity(mut self, opacity: f32) -> Self {
327 self.fill = self.fill.gamma_multiply(opacity);
328 self.stroke.color = self.stroke.color.gamma_multiply(opacity);
329 self.shadow.color = self.shadow.color.gamma_multiply(opacity);
330 self
331 }
332}
333
334/// ## Inspectors
335impl Frame {
336 /// How much extra space the frame uses up compared to the content.
337 ///
338 /// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
339 #[inline]
340 pub fn total_margin(&self) -> Marginf {
341 Marginf::from(self.inner_margin)
342 + Marginf::from(self.stroke.width)
343 + Marginf::from(self.outer_margin)
344 }
345
346 /// Calculate the `fill_rect` from the `content_rect`.
347 ///
348 /// This is the rectangle that is filled with the fill color (inside the stroke, if any).
349 pub fn fill_rect(&self, content_rect: Rect) -> Rect {
350 content_rect + self.inner_margin
351 }
352
353 /// Calculate the `widget_rect` from the `content_rect`.
354 ///
355 /// This is the visible and interactive rectangle.
356 pub fn widget_rect(&self, content_rect: Rect) -> Rect {
357 content_rect + self.inner_margin + Marginf::from(self.stroke.width)
358 }
359
360 /// Calculate the `outer_rect` from the `content_rect`.
361 ///
362 /// This is what is allocated in the outer [`Ui`], and is what is returned by [`Response::rect`].
363 pub fn outer_rect(&self, content_rect: Rect) -> Rect {
364 content_rect + self.inner_margin + Marginf::from(self.stroke.width) + self.outer_margin
365 }
366}
367
368// ----------------------------------------------------------------------------
369
370pub struct Prepared {
371 /// The frame that was prepared.
372 ///
373 /// The margin has already been read and used,
374 /// but the rest of the fields may be modified.
375 pub frame: Frame,
376
377 /// This is where we will insert the frame shape so it ends up behind the content.
378 where_to_put_background: ShapeIdx,
379
380 /// Add your widgets to this UI so it ends up within the frame.
381 pub content_ui: Ui,
382}
383
384impl Frame {
385 /// Begin a dynamically colored frame.
386 ///
387 /// This is a more advanced API.
388 /// Usually you want to use [`Self::show`] instead.
389 ///
390 /// See docs for [`Frame`] for an example.
391 pub fn begin(self, ui: &mut Ui) -> Prepared {
392 let where_to_put_background = ui.painter().add(Shape::Noop);
393 let outer_rect_bounds = ui.available_rect_before_wrap();
394
395 let mut max_content_rect = outer_rect_bounds - self.total_margin();
396
397 // Make sure we don't shrink to the negative:
398 max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x);
399 max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y);
400
401 let content_ui = ui.new_child(
402 UiBuilder::new()
403 .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self))
404 .max_rect(max_content_rect),
405 );
406
407 Prepared {
408 frame: self,
409 where_to_put_background,
410 content_ui,
411 }
412 }
413
414 /// Show the given ui surrounded by this frame.
415 pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
416 self.show_dyn(ui, Box::new(add_contents))
417 }
418
419 /// Show using dynamic dispatch.
420 pub fn show_dyn<'c, R>(
421 self,
422 ui: &mut Ui,
423 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
424 ) -> InnerResponse<R> {
425 let mut prepared = self.begin(ui);
426 let ret = add_contents(&mut prepared.content_ui);
427 let response = prepared.end(ui);
428 InnerResponse::new(ret, response)
429 }
430
431 /// Paint this frame as a shape.
432 pub fn paint(&self, content_rect: Rect) -> Shape {
433 let Self {
434 inner_margin: _,
435 fill,
436 stroke,
437 corner_radius,
438 outer_margin: _,
439 shadow,
440 } = *self;
441
442 let widget_rect = self.widget_rect(content_rect);
443
444 let frame_shape = Shape::Rect(epaint::RectShape::new(
445 widget_rect,
446 corner_radius,
447 fill,
448 stroke,
449 epaint::StrokeKind::Inside,
450 ));
451
452 if shadow == Default::default() {
453 frame_shape
454 } else {
455 let shadow = shadow.as_shape(widget_rect, corner_radius);
456 Shape::Vec(vec![Shape::from(shadow), frame_shape])
457 }
458 }
459}
460
461impl Prepared {
462 fn outer_rect(&self) -> Rect {
463 let content_rect = self.content_ui.min_rect();
464 content_rect
465 + self.frame.inner_margin
466 + Marginf::from(self.frame.stroke.width)
467 + self.frame.outer_margin
468 }
469
470 /// Allocate the space that was used by [`Self::content_ui`].
471 ///
472 /// This MUST be called, or the parent ui will not know how much space this widget used.
473 ///
474 /// This can be called before or after [`Self::paint`].
475 pub fn allocate_space(&self, ui: &mut Ui) -> Response {
476 ui.allocate_rect(self.outer_rect(), Sense::hover())
477 }
478
479 /// Paint the frame.
480 ///
481 /// This can be called before or after [`Self::allocate_space`].
482 pub fn paint(&self, ui: &Ui) {
483 let content_rect = self.content_ui.min_rect();
484 let widget_rect = self.frame.widget_rect(content_rect);
485
486 if ui.is_rect_visible(widget_rect) {
487 let shape = self.frame.paint(content_rect);
488 ui.painter().set(self.where_to_put_background, shape);
489 }
490 }
491
492 /// Convenience for calling [`Self::allocate_space`] and [`Self::paint`].
493 ///
494 /// Returns the outer rect, i.e. including the outer margin.
495 pub fn end(self, ui: &mut Ui) -> Response {
496 self.paint(ui);
497 self.allocate_space(ui)
498 }
499}