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}
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 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 #[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 #[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 #[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 #[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 #[inline]
182 pub fn selected(mut self, selected: bool) -> Self {
183 self.selected = selected;
184 self
185 }
186
187 #[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 #[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}