Skip to main content

egui_elements/
visuals.rs

1//! Per-widget visuals stored on [`Theme`](crate::theme::Theme) and optionally
2//! overridden on a single widget via `.visuals(...)`.
3//!
4//! Custom widgets resolve visuals in this order: the override → values
5//! written by [`Theme::install`](crate::theme::Theme::install) → stock
6//! [`egui::Style`].
7
8use egui::{
9   Color32, CornerRadius, Painter, Rect, Response, Shadow, Shape, Stroke, StrokeKind,
10   epaint::RectShape,
11};
12
13/// Alias: labels use the same visuals as buttons.
14pub type LabelVisuals = ButtonVisuals;
15
16/// Visuals for a button
17#[derive(Copy, Clone, Debug, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct ButtonVisuals {
20   /// Glyph / label color.
21   pub text: Color32,
22   /// Idle fill.
23   pub bg: Color32,
24   /// Fill while hovered.
25   pub bg_hover: Color32,
26   /// Fill while the pointer is down.
27   pub bg_click: Color32,
28   /// Fill when [`crate::widgets::Button::selected`] is set.
29   pub bg_selected: Color32,
30   /// Idle border.
31   pub border: Stroke,
32   /// Border while hovered.
33   pub border_hover: Stroke,
34   /// Border while the pointer is down.
35   pub border_click: Stroke,
36   pub corner_radius: CornerRadius,
37   pub shadow: Shadow,
38}
39
40impl PartialEq for ButtonVisuals {
41   fn eq(&self, other: &Self) -> bool {
42      self.text == other.text
43         && self.bg == other.bg
44         && self.bg_hover == other.bg_hover
45         && self.bg_click == other.bg_click
46         && self.bg_selected == other.bg_selected
47         && self.border == other.border
48         && self.border_hover == other.border_hover
49         && self.border_click == other.border_click
50         && self.corner_radius == other.corner_radius
51         && self.shadow == other.shadow
52   }
53}
54
55impl Eq for ButtonVisuals {}
56
57impl ButtonVisuals {
58   /// Fill for the current interaction state (click > hover > idle).
59   pub fn bg_from_res(&self, res: &Response) -> Color32 {
60      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
61         self.bg_click
62      } else if res.hovered() || res.highlighted() {
63         self.bg_hover
64      } else {
65         self.bg
66      }
67   }
68
69   /// Border for the current interaction state (click > hover > idle).
70   pub fn border_from_res(&self, res: &Response) -> Stroke {
71      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
72         self.border_click
73      } else if res.hovered() || res.highlighted() {
74         self.border_hover
75      } else {
76         self.border
77      }
78   }
79}
80
81/// Visuals for a TextEdit
82#[derive(Copy, Clone, Debug, Default)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct TextEditVisuals {
85   /// Typed text color.
86   pub text: Color32,
87   /// Field fill.
88   pub bg: Color32,
89   /// Idle border.
90   pub border: Stroke,
91   /// Border while hovered.
92   pub border_hover: Stroke,
93   /// Border while focused / open.
94   pub border_open: Stroke,
95   pub corner_radius: CornerRadius,
96   pub shadow: Shadow,
97}
98
99impl PartialEq for TextEditVisuals {
100   fn eq(&self, other: &Self) -> bool {
101      self.text == other.text
102         && self.bg == other.bg
103         && self.border == other.border
104         && self.border_hover == other.border_hover
105         && self.border_open == other.border_open
106         && self.corner_radius == other.corner_radius
107         && self.shadow == other.shadow
108   }
109}
110
111impl Eq for TextEditVisuals {}
112
113impl TextEditVisuals {
114   /// Border for the current interaction state (open > hover > idle).
115   pub fn border_from_res(&self, res: &Response) -> Stroke {
116      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
117         self.border_open
118      } else if res.hovered() || res.highlighted() {
119         self.border_hover
120      } else {
121         self.border
122      }
123   }
124}
125
126/// Visuals for a ComboBox
127#[derive(Copy, Clone, Debug, Default)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129pub struct ComboBoxVisuals {
130   /// Idle fill of the closed button.
131   pub bg: Color32,
132   /// Disclosure-icon color.
133   pub icon: Color32,
134   /// Fill while hovered.
135   pub bg_hover: Color32,
136   /// Fill while the popup is open.
137   pub bg_open: Color32,
138   /// Idle border.
139   pub border: Stroke,
140   /// Border while hovered.
141   pub border_hover: Stroke,
142   /// Border while the popup is open.
143   pub border_open: Stroke,
144   pub corner_radius: CornerRadius,
145   pub shadow: Shadow,
146}
147
148impl PartialEq for ComboBoxVisuals {
149   fn eq(&self, other: &Self) -> bool {
150      self.bg == other.bg
151         && self.icon == other.icon
152         && self.bg_hover == other.bg_hover
153         && self.bg_open == other.bg_open
154         && self.border == other.border
155         && self.border_hover == other.border_hover
156         && self.border_open == other.border_open
157         && self.corner_radius == other.corner_radius
158         && self.shadow == other.shadow
159   }
160}
161
162impl Eq for ComboBoxVisuals {}
163
164impl ComboBoxVisuals {
165   /// Fill for the current interaction state (open > hover > idle).
166   pub fn bg_from_res(&self, res: &Response) -> Color32 {
167      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
168         self.bg_open
169      } else if res.hovered() || res.highlighted() {
170         self.bg_hover
171      } else {
172         self.bg
173      }
174   }
175
176   /// Border for the current interaction state (open > hover > idle).
177   pub fn border_from_res(&self, res: &Response) -> Stroke {
178      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
179         self.border_open
180      } else if res.hovered() || res.highlighted() {
181         self.border_hover
182      } else {
183         self.border
184      }
185   }
186}
187
188/// Visuals for Frame
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190#[derive(Clone, Copy, Debug, Default)]
191pub struct FrameVisuals {
192   pub bg: Color32,
193   pub bg_hover: Color32,
194   pub bg_click: Color32,
195   /// Fill when [`crate::widgets::Frame::selected`] is set.
196   pub bg_selected: Color32,
197   pub border: Stroke,
198   pub border_hover: Stroke,
199   pub border_click: Stroke,
200   pub corner_radius: CornerRadius,
201   pub shadow: Shadow,
202}
203
204impl PartialEq for FrameVisuals {
205   fn eq(&self, other: &Self) -> bool {
206      self.bg == other.bg
207         && self.bg_hover == other.bg_hover
208         && self.bg_click == other.bg_click
209         && self.bg_selected == other.bg_selected
210         && self.border == other.border
211         && self.border_hover == other.border_hover
212         && self.border_click == other.border_click
213         && self.corner_radius == other.corner_radius
214         && self.shadow == other.shadow
215   }
216}
217
218impl Eq for FrameVisuals {}
219
220impl FrameVisuals {
221   pub fn bg_from_res(&self, res: &Response) -> Color32 {
222      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
223         self.bg_click
224      } else if res.contains_pointer() || res.hovered() || res.highlighted() {
225         self.bg_hover
226      } else {
227         self.bg
228      }
229   }
230
231   pub fn border_from_res(&self, res: &Response) -> Stroke {
232      if res.is_pointer_button_down_on() || res.has_focus() || res.clicked() {
233         self.border_click
234      } else if res.contains_pointer() || res.hovered() || res.highlighted() {
235         self.border_hover
236      } else {
237         self.border
238      }
239   }
240
241   /// Shadow + fill + inside stroke on an already allocated widget rect.
242   pub fn paint_at(&self, painter: &Painter, rect: Rect, fill: Color32, stroke: Stroke) {
243      painter.add(paint_shape(
244         rect,
245         fill,
246         stroke,
247         self.corner_radius,
248         self.shadow,
249      ));
250   }
251}
252
253pub(crate) fn paint_shape(
254   rect: Rect,
255   fill: Color32,
256   stroke: Stroke,
257   corner_radius: CornerRadius,
258   shadow: Shadow,
259) -> Shape {
260   if rect.width() <= 0.0 || rect.height() <= 0.0 {
261      return Shape::Noop;
262   }
263
264   let has_fill = fill != Color32::TRANSPARENT;
265
266   let has_shadow = shadow != Shadow::NONE && shadow.color != Color32::TRANSPARENT;
267   if !has_fill && stroke.is_empty() && !has_shadow {
268      return Shape::Noop;
269   }
270
271   let mut shapes = Vec::new();
272
273   if has_shadow {
274      shapes.push(Shape::from(shadow.as_shape(rect, corner_radius)));
275   }
276
277   if has_fill || !stroke.is_empty() {
278      shapes.push(Shape::Rect(RectShape::new(
279         rect,
280         corner_radius,
281         fill,
282         stroke,
283         StrokeKind::Inside,
284      )));
285   }
286
287   match shapes.len() {
288      0 => Shape::Noop,
289      1 => shapes.remove(0),
290      _ => Shape::Vec(shapes),
291   }
292}
293
294#[cfg(test)]
295mod tests {
296   use super::*;
297
298   #[test]
299   fn paint_shape_draws_fill_without_stroke() {
300      let shape = paint_shape(
301         Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(10.0, 10.0)),
302         Color32::RED,
303         Stroke::NONE,
304         CornerRadius::ZERO,
305         Shadow::NONE,
306      );
307      match shape {
308         Shape::Rect(rect) => assert_eq!(rect.fill, Color32::RED),
309         other => panic!("expected filled rect, got {other:?}"),
310      }
311   }
312}