Skip to main content

egui_elements/widgets/
frame.rs

1use egui::layers::ShapeIdx;
2use egui::{
3   Color32, CornerRadius, InnerResponse, Margin, Rect, Response, Sense, Shadow, Shape, Stroke, Ui,
4   UiBuilder, UiKind, UiStackInfo, epaint::MarginF32,
5};
6
7use crate::visuals::FrameVisuals;
8
9/// A frame around some content: margin, fill ([`Fill`]), stroke, rounding, shadow.
10///
11/// Same geometry as [`egui::Frame`]:
12/// `content_size + inner_margin + 2 * stroke.width + outer_margin`.
13///
14/// The difference is the fill is a [`Fill`], so it can be a two-stop gradient,
15/// and state colors live on [`FrameVisuals`] like the other widgets.
16///
17/// Stroke width is taken from the idle border at `begin`. Changing stroke
18/// width on hover after `begin` does not relayout.
19#[must_use = "You should call .show()"]
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct Frame {
22   pub inner_margin: Margin,
23   pub outer_margin: Margin,
24   pub visuals: FrameVisuals,
25   pub sense: Sense,
26   /// When `false`, paint always uses idle fill / border, even if the
27   /// pointer is over the frame. Hover / click fills need [`Self::interactive`].
28   pub interactive: bool,
29   /// Paint [`FrameVisuals::bg_selected`] instead of idle / hover / click.
30   pub selected: bool,
31   /// Stretch the frame to the available width (useful for sidebar rows).
32   pub fill_width: bool,
33}
34
35impl Default for Frame {
36   fn default() -> Self {
37      Self::NONE
38   }
39}
40
41impl Frame {
42   pub const NONE: Self = Self {
43      inner_margin: Margin::ZERO,
44      outer_margin: Margin::ZERO,
45      visuals: FrameVisuals {
46         bg: Color32::TRANSPARENT,
47         bg_hover: Color32::TRANSPARENT,
48         bg_click: Color32::TRANSPARENT,
49         bg_selected: Color32::TRANSPARENT,
50         border: Stroke::NONE,
51         border_hover: Stroke::NONE,
52         border_click: Stroke::NONE,
53         corner_radius: CornerRadius::ZERO,
54         shadow: Shadow::NONE,
55      },
56      sense: Sense::HOVER,
57      interactive: false,
58      selected: false,
59      fill_width: false,
60   };
61
62   pub const fn new() -> Self {
63      Self::NONE
64   }
65
66   /// Copy margins, idle fill, stroke, radius, and shadow from an [`egui::Frame`].
67   /// Hover / click start equal to idle.
68   pub fn from_egui(frame: egui::Frame) -> Self {
69      let fill = frame.fill;
70      Self {
71         inner_margin: frame.inner_margin,
72         outer_margin: frame.outer_margin,
73         visuals: FrameVisuals {
74            bg: fill,
75            bg_hover: fill,
76            bg_click: fill,
77            bg_selected: fill,
78            border: frame.stroke,
79            border_hover: frame.stroke,
80            border_click: frame.stroke,
81            corner_radius: frame.corner_radius,
82            shadow: frame.shadow,
83         },
84         sense: Sense::HOVER,
85         interactive: false,
86         selected: false,
87         fill_width: false,
88      }
89   }
90
91   #[inline]
92   pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
93      self.inner_margin = inner_margin.into();
94      self
95   }
96
97   #[inline]
98   pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
99      self.outer_margin = outer_margin.into();
100      self
101   }
102
103   /// Idle fill. Does not change hover / click unless they were still the old idle fill.
104   #[inline]
105   pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
106      let fill = fill.into();
107      let old = self.visuals.bg;
108      self.visuals.bg = fill;
109      if self.visuals.bg_hover == old {
110         self.visuals.bg_hover = fill;
111      }
112      if self.visuals.bg_click == old {
113         self.visuals.bg_click = fill;
114      }
115      if self.visuals.bg_selected == old {
116         self.visuals.bg_selected = fill;
117      }
118      self
119   }
120
121   #[inline]
122   pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
123      let stroke = stroke.into();
124      let old = self.visuals.border;
125      self.visuals.border = stroke;
126      if self.visuals.border_hover == old {
127         self.visuals.border_hover = stroke;
128      }
129      if self.visuals.border_click == old {
130         self.visuals.border_click = stroke;
131      }
132      self
133   }
134
135   #[inline]
136   pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
137      self.visuals.corner_radius = corner_radius.into();
138      self
139   }
140
141   /// Make the frame square corners.
142   #[inline]
143   pub fn square_corners(mut self) -> Self {
144      self.visuals.corner_radius = CornerRadius::same(0);
145      self
146   }
147
148   #[inline]
149   pub fn shadow(mut self, shadow: Shadow) -> Self {
150      self.visuals.shadow = shadow;
151      self
152   }
153
154   /// Replace fill, stroke, radius, and shadow. Margins and sense are kept.
155   #[inline]
156   pub fn visuals(mut self, visuals: FrameVisuals) -> Self {
157      self.visuals = visuals;
158      self
159   }
160
161   #[inline]
162   pub fn sense(mut self, sense: Sense) -> Self {
163      self.sense = sense;
164      self
165   }
166
167   /// `true` → [`Sense::click`] and hover / click fills. `false` → idle chrome
168   /// only (pointer hover does not swap fill).
169   #[inline]
170   pub fn interactive(mut self, interactive: bool) -> Self {
171      self.interactive = interactive;
172      self.sense = if interactive {
173         Sense::click()
174      } else {
175         Sense::HOVER
176      };
177      self
178   }
179
180   /// Paint [`FrameVisuals::bg_selected`] even when not hovered.
181   #[inline]
182   pub fn selected(mut self, selected: bool) -> Self {
183      self.selected = selected;
184      self
185   }
186
187   /// Stretch the frame to the available width.
188   ///
189   /// The content `Ui` also gets that min width, so inner
190   /// right-to-left rows (unread counts, timestamps) land on the far edge.
191   #[inline]
192   pub fn fill_width(mut self, fill: bool) -> Self {
193      self.fill_width = fill;
194      self
195   }
196
197   #[inline]
198   pub fn multiply_with_opacity(mut self, opacity: f32) -> Self {
199      self.visuals.bg = self.visuals.bg.gamma_multiply(opacity);
200      self.visuals.bg_hover = self.visuals.bg_hover.gamma_multiply(opacity);
201      self.visuals.bg_click = self.visuals.bg_click.gamma_multiply(opacity);
202      self.visuals.bg_selected = self.visuals.bg_selected.gamma_multiply(opacity);
203      self.visuals.border.color = self.visuals.border.color.gamma_multiply(opacity);
204      self.visuals.border_hover.color = self.visuals.border_hover.color.gamma_multiply(opacity);
205      self.visuals.border_click.color = self.visuals.border_click.color.gamma_multiply(opacity);
206      self.visuals.shadow.color = self.visuals.shadow.color.gamma_multiply(opacity);
207      self
208   }
209
210   /// Idle stroke width is part of the margin (same as [`egui::Frame`]).
211   #[inline]
212   pub fn total_margin(&self) -> MarginF32 {
213      MarginF32::from(self.inner_margin)
214         + MarginF32::from(self.visuals.border.width)
215         + MarginF32::from(self.outer_margin)
216   }
217
218   pub fn fill_rect(&self, content_rect: Rect) -> Rect {
219      content_rect + self.inner_margin
220   }
221
222   pub fn widget_rect(&self, content_rect: Rect) -> Rect {
223      content_rect + self.inner_margin + MarginF32::from(self.visuals.border.width)
224   }
225
226   pub fn outer_rect(&self, content_rect: Rect) -> Rect {
227      content_rect
228         + self.inner_margin
229         + MarginF32::from(self.visuals.border.width)
230         + self.outer_margin
231   }
232
233   fn stack_frame(&self) -> egui::Frame {
234      egui::Frame {
235         inner_margin: self.inner_margin,
236         fill: Color32::TRANSPARENT,
237         stroke: Stroke::new(self.visuals.border.width, Color32::TRANSPARENT),
238         corner_radius: self.visuals.corner_radius,
239         outer_margin: self.outer_margin,
240         shadow: self.visuals.shadow,
241      }
242   }
243
244   fn paint_shape(&self, widget_rect: Rect, fill: Color32, stroke: Stroke) -> Shape {
245      crate::visuals::paint_shape(
246         widget_rect,
247         fill,
248         stroke,
249         self.visuals.corner_radius,
250         self.visuals.shadow,
251      )
252   }
253}
254
255pub struct Prepared {
256   pub frame: Frame,
257   where_to_put_background: ShapeIdx,
258   pub content_ui: Ui,
259   max_content_rect: Rect,
260}
261
262impl Frame {
263   pub fn begin(self, ui: &mut Ui) -> Prepared {
264      let where_to_put_background = ui.painter().add(Shape::Noop);
265      let outer_rect_bounds = ui.available_rect_before_wrap();
266
267      let mut max_content_rect = outer_rect_bounds - self.total_margin();
268      max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x);
269      max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y);
270
271      let mut content_ui = ui.new_child(
272         UiBuilder::new()
273            .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self.stack_frame()))
274            .max_rect(max_content_rect)
275            .sense(Sense::empty()),
276      );
277      if self.fill_width {
278         content_ui.set_min_width(max_content_rect.width());
279      }
280
281      Prepared {
282         frame: self,
283         where_to_put_background,
284         content_ui,
285         max_content_rect,
286      }
287   }
288
289   pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
290      let mut prepared = self.begin(ui);
291      let ret = add_contents(&mut prepared.content_ui);
292      let response = prepared.end(ui);
293      InnerResponse::new(ret, response)
294   }
295}
296
297impl Prepared {
298   fn content_rect(&self) -> Rect {
299      let mut content_rect = self.content_ui.min_rect();
300      if self.frame.fill_width {
301         content_rect.min.x = content_rect.min.x.min(self.max_content_rect.min.x);
302         content_rect.max.x = content_rect.max.x.max(self.max_content_rect.max.x);
303      }
304      content_rect
305   }
306
307   fn allocated_outer_rect(&self) -> Rect {
308      self.frame.outer_rect(self.content_rect())
309   }
310
311   pub fn allocate_space(&self, ui: &mut Ui) -> Response {
312      ui.allocate_rect(self.allocated_outer_rect(), self.frame.sense)
313   }
314
315   pub fn paint(&self, ui: &Ui, response: &Response) {
316      let content_rect = self.content_rect();
317      let widget_rect = self.frame.widget_rect(content_rect);
318      if !ui.is_rect_visible(widget_rect) {
319         return;
320      }
321      let (fill, stroke) = if self.frame.selected {
322         (self.frame.visuals.bg_selected, self.frame.visuals.border)
323      } else if self.frame.interactive {
324         (
325            self.frame.visuals.bg_from_res(response),
326            self.frame.visuals.border_from_res(response),
327         )
328      } else {
329         (self.frame.visuals.bg, self.frame.visuals.border)
330      };
331      let shape = self.frame.paint_shape(widget_rect, fill, stroke);
332      ui.painter().set(self.where_to_put_background, shape);
333   }
334
335   pub fn end(self, ui: &mut Ui) -> Response {
336      let response = self.allocate_space(ui);
337      self.paint(ui, &response);
338      response
339   }
340}
341
342#[cfg(test)]
343mod tests {
344   use super::*;
345
346   #[test]
347   fn total_margin_sums_parts() {
348      let frame = Frame::new()
349         .inner_margin(Margin::same(4))
350         .outer_margin(Margin::same(3))
351         .stroke(Stroke::new(2.0, Color32::WHITE));
352      let m = frame.total_margin();
353      assert_eq!(m.left, 9.0);
354      assert_eq!(m.right, 9.0);
355      assert_eq!(m.top, 9.0);
356      assert_eq!(m.bottom, 9.0);
357   }
358
359   #[test]
360   fn fill_propagates_to_matching_states() {
361      let frame = Frame::new().fill(Color32::RED);
362      assert_eq!(frame.visuals.bg, Color32::RED);
363      assert_eq!(frame.visuals.bg_hover, Color32::RED);
364      assert_eq!(frame.visuals.bg_click, Color32::RED);
365      assert_eq!(frame.visuals.bg_selected, Color32::RED);
366   }
367
368   #[test]
369   fn interactive_false_is_default() {
370      let frame = Frame::new().fill(Color32::RED);
371      assert!(!frame.interactive);
372      assert!(!frame.selected);
373      assert!(!frame.fill_width);
374      assert!(frame.interactive(true).interactive);
375      assert!(!frame.interactive(true).interactive(false).interactive);
376      assert!(frame.selected(true).selected);
377      assert!(frame.fill_width(true).fill_width);
378   }
379}