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#[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 pub interactive: bool,
29 pub selected: bool,
31 pub fill_width: bool,
33 corner_radius_override: Option<CornerRadius>,
36}
37
38impl Default for Frame {
39 fn default() -> Self {
40 Self::NONE
41 }
42}
43
44impl Frame {
45 pub const NONE: Self = Self {
46 inner_margin: Margin::ZERO,
47 outer_margin: Margin::ZERO,
48 visuals: FrameVisuals {
49 bg: Color32::TRANSPARENT,
50 bg_hover: Color32::TRANSPARENT,
51 bg_click: Color32::TRANSPARENT,
52 bg_selected: Color32::TRANSPARENT,
53 border: Stroke::NONE,
54 border_hover: Stroke::NONE,
55 border_click: Stroke::NONE,
56 corner_radius: CornerRadius::ZERO,
57 shadow: Shadow::NONE,
58 },
59 sense: Sense::HOVER,
60 interactive: false,
61 selected: false,
62 fill_width: false,
63 corner_radius_override: None,
64 };
65
66 pub const fn new() -> Self {
67 Self::NONE
68 }
69
70 pub fn from_egui(frame: egui::Frame) -> Self {
73 let fill = frame.fill;
74 Self {
75 inner_margin: frame.inner_margin,
76 outer_margin: frame.outer_margin,
77 visuals: FrameVisuals {
78 bg: fill,
79 bg_hover: fill,
80 bg_click: fill,
81 bg_selected: fill,
82 border: frame.stroke,
83 border_hover: frame.stroke,
84 border_click: frame.stroke,
85 corner_radius: frame.corner_radius,
86 shadow: frame.shadow,
87 },
88 sense: Sense::HOVER,
89 interactive: false,
90 selected: false,
91 fill_width: false,
92 corner_radius_override: None,
93 }
94 }
95
96 #[inline]
97 pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
98 self.inner_margin = inner_margin.into();
99 self
100 }
101
102 #[inline]
103 pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
104 self.outer_margin = outer_margin.into();
105 self
106 }
107
108 #[inline]
110 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
111 let fill = fill.into();
112 let old = self.visuals.bg;
113 self.visuals.bg = fill;
114 if self.visuals.bg_hover == old {
115 self.visuals.bg_hover = fill;
116 }
117 if self.visuals.bg_click == old {
118 self.visuals.bg_click = fill;
119 }
120 if self.visuals.bg_selected == old {
121 self.visuals.bg_selected = fill;
122 }
123 self
124 }
125
126 #[inline]
127 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
128 let stroke = stroke.into();
129 let old = self.visuals.border;
130 self.visuals.border = stroke;
131 if self.visuals.border_hover == old {
132 self.visuals.border_hover = stroke;
133 }
134 if self.visuals.border_click == old {
135 self.visuals.border_click = stroke;
136 }
137 self
138 }
139
140 #[inline]
141 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
142 let corner_radius = corner_radius.into();
143 self.visuals.corner_radius = corner_radius;
144 self.corner_radius_override = Some(corner_radius);
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 #[inline]
158 pub fn visuals(mut self, visuals: FrameVisuals) -> Self {
159 self.visuals = visuals;
160 if let Some(corner_radius) = self.corner_radius_override {
161 self.visuals.corner_radius = corner_radius;
162 }
163 self
164 }
165
166 #[inline]
167 pub fn sense(mut self, sense: Sense) -> Self {
168 self.sense = sense;
169 self
170 }
171
172 #[inline]
175 pub fn interactive(mut self, interactive: bool) -> Self {
176 self.interactive = interactive;
177 self.sense = if interactive {
178 Sense::click()
179 } else {
180 Sense::HOVER
181 };
182 self
183 }
184
185 #[inline]
187 pub fn selected(mut self, selected: bool) -> Self {
188 self.selected = selected;
189 self
190 }
191
192 #[inline]
197 pub fn fill_width(mut self, fill: bool) -> Self {
198 self.fill_width = fill;
199 self
200 }
201
202 #[inline]
203 pub fn multiply_with_opacity(mut self, opacity: f32) -> Self {
204 self.visuals.bg = self.visuals.bg.gamma_multiply(opacity);
205 self.visuals.bg_hover = self.visuals.bg_hover.gamma_multiply(opacity);
206 self.visuals.bg_click = self.visuals.bg_click.gamma_multiply(opacity);
207 self.visuals.bg_selected = self.visuals.bg_selected.gamma_multiply(opacity);
208 self.visuals.border.color = self.visuals.border.color.gamma_multiply(opacity);
209 self.visuals.border_hover.color = self.visuals.border_hover.color.gamma_multiply(opacity);
210 self.visuals.border_click.color = self.visuals.border_click.color.gamma_multiply(opacity);
211 self.visuals.shadow.color = self.visuals.shadow.color.gamma_multiply(opacity);
212 self
213 }
214
215 #[inline]
217 pub fn total_margin(&self) -> MarginF32 {
218 MarginF32::from(self.inner_margin)
219 + MarginF32::from(self.visuals.border.width)
220 + MarginF32::from(self.outer_margin)
221 }
222
223 pub fn fill_rect(&self, content_rect: Rect) -> Rect {
224 content_rect + self.inner_margin
225 }
226
227 pub fn widget_rect(&self, content_rect: Rect) -> Rect {
228 content_rect + self.inner_margin + MarginF32::from(self.visuals.border.width)
229 }
230
231 pub fn outer_rect(&self, content_rect: Rect) -> Rect {
232 content_rect
233 + self.inner_margin
234 + MarginF32::from(self.visuals.border.width)
235 + self.outer_margin
236 }
237
238 fn stack_frame(&self) -> egui::Frame {
239 egui::Frame {
240 inner_margin: self.inner_margin,
241 fill: Color32::TRANSPARENT,
242 stroke: Stroke::new(self.visuals.border.width, Color32::TRANSPARENT),
243 corner_radius: self.visuals.corner_radius,
244 outer_margin: self.outer_margin,
245 shadow: self.visuals.shadow,
246 }
247 }
248
249 fn paint_shape(&self, widget_rect: Rect, fill: Color32, stroke: Stroke) -> Shape {
250 crate::visuals::paint_shape(
251 widget_rect,
252 fill,
253 stroke,
254 self.visuals.corner_radius,
255 self.visuals.shadow,
256 )
257 }
258}
259
260pub struct Prepared {
261 pub frame: Frame,
262 where_to_put_background: ShapeIdx,
263 pub content_ui: Ui,
264 max_content_rect: Rect,
265}
266
267impl Frame {
268 pub fn begin(self, ui: &mut Ui) -> Prepared {
269 let where_to_put_background = ui.painter().add(Shape::Noop);
270 let outer_rect_bounds = ui.available_rect_before_wrap();
271
272 let mut max_content_rect = outer_rect_bounds - self.total_margin();
273 max_content_rect.max.x = max_content_rect.max.x.max(max_content_rect.min.x);
274 max_content_rect.max.y = max_content_rect.max.y.max(max_content_rect.min.y);
275
276 let mut content_ui = ui.new_child(
277 UiBuilder::new()
278 .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self.stack_frame()))
279 .max_rect(max_content_rect)
280 .sense(Sense::empty()),
281 );
282 if self.fill_width {
283 content_ui.set_min_width(max_content_rect.width());
284 }
285
286 Prepared {
287 frame: self,
288 where_to_put_background,
289 content_ui,
290 max_content_rect,
291 }
292 }
293
294 pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
295 let mut prepared = self.begin(ui);
296 let ret = add_contents(&mut prepared.content_ui);
297 let response = prepared.end(ui);
298 InnerResponse::new(ret, response)
299 }
300}
301
302impl Prepared {
303 fn content_rect(&self) -> Rect {
304 let mut content_rect = self.content_ui.min_rect();
305 if self.frame.fill_width {
306 content_rect.min.x = content_rect.min.x.min(self.max_content_rect.min.x);
307 content_rect.max.x = content_rect.max.x.max(self.max_content_rect.max.x);
308 }
309 content_rect
310 }
311
312 fn allocated_outer_rect(&self) -> Rect {
313 self.frame.outer_rect(self.content_rect())
314 }
315
316 pub fn allocate_space(&self, ui: &mut Ui) -> Response {
317 ui.allocate_rect(self.allocated_outer_rect(), self.frame.sense)
318 }
319
320 pub fn paint(&self, ui: &Ui, response: &Response) {
321 let content_rect = self.content_rect();
322 let widget_rect = self.frame.widget_rect(content_rect);
323 if !ui.is_rect_visible(widget_rect) {
324 return;
325 }
326 let (fill, stroke) = if self.frame.selected {
327 (
328 self.frame.visuals.bg_selected,
329 self.frame.visuals.border,
330 )
331 } else if self.frame.interactive {
332 (
333 self.frame.visuals.bg_from_res(response),
334 self.frame.visuals.border_from_res(response),
335 )
336 } else {
337 (self.frame.visuals.bg, self.frame.visuals.border)
338 };
339 let shape = self.frame.paint_shape(widget_rect, fill, stroke);
340 ui.painter().set(self.where_to_put_background, shape);
341 }
342
343 pub fn end(self, ui: &mut Ui) -> Response {
344 let response = self.allocate_space(ui);
345 self.paint(ui, &response);
346 response
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn total_margin_sums_parts() {
356 let frame = Frame::new()
357 .inner_margin(Margin::same(4))
358 .outer_margin(Margin::same(3))
359 .stroke(Stroke::new(2.0, Color32::WHITE));
360 let m = frame.total_margin();
361 assert_eq!(m.left, 9.0);
362 assert_eq!(m.right, 9.0);
363 assert_eq!(m.top, 9.0);
364 assert_eq!(m.bottom, 9.0);
365 }
366
367 #[test]
368 fn fill_propagates_to_matching_states() {
369 let frame = Frame::new().fill(Color32::RED);
370 assert_eq!(frame.visuals.bg, Color32::RED);
371 assert_eq!(frame.visuals.bg_hover, Color32::RED);
372 assert_eq!(frame.visuals.bg_click, Color32::RED);
373 assert_eq!(frame.visuals.bg_selected, Color32::RED);
374 }
375
376 #[test]
377 fn interactive_false_is_default() {
378 let frame = Frame::new().fill(Color32::RED);
379 assert!(!frame.interactive);
380 assert!(!frame.selected);
381 assert!(!frame.fill_width);
382 assert!(frame.interactive(true).interactive);
383 assert!(!frame.interactive(true).interactive(false).interactive);
384 assert!(frame.selected(true).selected);
385 assert!(frame.fill_width(true).fill_width);
386 }
387
388 fn theme_visuals() -> FrameVisuals {
389 FrameVisuals {
390 bg: Color32::RED,
391 bg_hover: Color32::BLUE,
392 bg_click: Color32::RED,
393 bg_selected: Color32::GREEN,
394 border: Stroke::NONE,
395 border_hover: Stroke::NONE,
396 border_click: Stroke::NONE,
397 corner_radius: CornerRadius::same(6),
398 shadow: Shadow::NONE,
399 }
400 }
401
402 #[test]
403 fn zero_radius_survives_later_visuals() {
404 let frame = Frame::new().corner_radius(0).visuals(theme_visuals());
405 assert_eq!(frame.visuals.corner_radius, CornerRadius::ZERO);
406 }
407
408 #[test]
409 fn corner_radius_zero_survives_later_visuals() {
410 let frame = Frame::new().corner_radius(0).visuals(theme_visuals());
411 assert_eq!(frame.visuals.corner_radius, CornerRadius::ZERO);
412 }
413
414 #[test]
415 fn visuals_then_zero_corner_radius() {
416 let frame = Frame::new().visuals(theme_visuals()).corner_radius(0);
417 assert_eq!(frame.visuals.corner_radius, CornerRadius::ZERO);
418 }
419
420 #[test]
421 fn visuals_without_override_keeps_theme_radius() {
422 let frame = Frame::new().visuals(theme_visuals());
423 assert_eq!(frame.visuals.corner_radius, CornerRadius::same(6));
424 }
425}