Skip to main content

egui/
widget_style.rs

1use std::{borrow::Cow, fmt};
2
3use emath::Vec2;
4use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
5use smallvec::SmallVec;
6
7use crate::{
8    Frame, Response, Style, TextBuffer as _, TextStyle,
9    style::{WidgetVisuals, Widgets},
10};
11
12/// General text style
13pub struct TextVisuals {
14    /// Font used
15    pub font_id: FontId,
16
17    /// Font color
18    pub color: Color32,
19
20    /// Text decoration
21    pub underline: Stroke,
22    pub strikethrough: Stroke,
23}
24
25/// General widget style
26pub struct WidgetStyle {
27    pub frame: Frame,
28
29    pub text: TextVisuals,
30
31    pub stroke: Stroke,
32}
33
34/// Dedicated button style
35pub struct ButtonStyle {
36    pub frame: Frame,
37    pub text_style: TextVisuals,
38}
39
40/// Dedicated checkbox style
41pub struct CheckboxStyle {
42    /// Frame around
43    pub frame: Frame,
44
45    /// Text next to it
46    pub text_style: TextVisuals,
47
48    /// Checkbox size
49    pub checkbox_size: f32,
50
51    /// Checkmark size
52    pub check_size: f32,
53
54    /// Frame of the checkbox itself
55    pub checkbox_frame: Frame,
56
57    /// Checkmark stroke
58    pub check_stroke: Stroke,
59}
60
61/// Dedicated label style
62pub struct LabelStyle {
63    /// Frame around
64    pub frame: Frame,
65
66    /// Text style
67    pub text: TextVisuals,
68
69    /// Wrap mode used
70    pub wrap_mode: TextWrapMode,
71}
72
73/// Dedicated separator style
74pub struct SeparatorStyle {
75    /// How much space is allocated in the layout direction
76    pub spacing: f32,
77
78    /// How to paint it
79    pub stroke: Stroke,
80}
81
82/// The different state of a widget can be
83#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
84pub enum WidgetState {
85    Noninteractive,
86    #[default]
87    Inactive,
88    Hovered,
89    Active,
90}
91
92impl Widgets {
93    /// The widget visuals according to the state
94    pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
95        match state {
96            WidgetState::Noninteractive => &self.noninteractive,
97            WidgetState::Inactive => &self.inactive,
98            WidgetState::Hovered => &self.hovered,
99            WidgetState::Active => &self.active,
100        }
101    }
102}
103
104impl Response {
105    pub fn widget_state(&self) -> WidgetState {
106        if !self.sense.interactive() {
107            WidgetState::Noninteractive
108        } else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
109            WidgetState::Active
110        } else if self.hovered() || self.highlighted() {
111            WidgetState::Hovered
112        } else {
113            WidgetState::Inactive
114        }
115    }
116}
117
118impl Style {
119    /// The general widget style. The style is computed according to the classes and state of the widget.
120    pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
121        let visuals = self.visuals.widgets.state(state);
122        let font_id = self.override_font_id.clone();
123        WidgetStyle {
124            frame: Frame {
125                fill: visuals.bg_fill,
126                stroke: visuals.bg_stroke,
127                corner_radius: visuals.corner_radius,
128                inner_margin: self.spacing.button_padding.into(),
129                ..Default::default()
130            },
131            stroke: visuals.fg_stroke,
132            text: TextVisuals {
133                color: self
134                    .visuals
135                    .override_text_color
136                    .unwrap_or_else(|| visuals.text_color()),
137                font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(self)),
138                strikethrough: Stroke::NONE,
139                underline: Stroke::NONE,
140            },
141        }
142    }
143
144    /// The dedicated button style. The style is computed according to the classes and state of the widget.
145    /// It depend on the general widget style.
146    pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
147        let mut visuals = *self.visuals.widgets.state(state);
148        let mut ws = self.widget_style(classes, state);
149
150        if classes.has(SELECTED_CLASS) {
151            visuals.weak_bg_fill = self.visuals.selection.bg_fill;
152            visuals.bg_fill = self.visuals.selection.bg_fill;
153            visuals.fg_stroke = self.visuals.selection.stroke;
154            ws.text.color = self.visuals.selection.stroke.color;
155        }
156
157        ButtonStyle {
158            frame: Frame {
159                fill: visuals.weak_bg_fill,
160                stroke: visuals.bg_stroke,
161                corner_radius: visuals.corner_radius,
162                outer_margin: (-Vec2::splat(visuals.expansion)).into(),
163                inner_margin: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
164                    - Vec2::splat(visuals.bg_stroke.width))
165                .into(),
166                ..Default::default()
167            },
168            text_style: ws.text,
169        }
170    }
171
172    /// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
173    /// It depend on the general widget style.
174    pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
175        let visuals = self.visuals.widgets.state(state);
176        let ws = self.widget_style(classes, state);
177        CheckboxStyle {
178            frame: Frame::new(),
179            checkbox_size: self.spacing.icon_width,
180            check_size: self.spacing.icon_width_inner,
181            checkbox_frame: Frame {
182                fill: visuals.bg_fill,
183                corner_radius: visuals.corner_radius,
184                stroke: visuals.bg_stroke,
185                ..Default::default()
186            },
187            text_style: ws.text,
188            check_stroke: ws.stroke,
189        }
190    }
191
192    /// The dedicated label style. The style is computed according to the classes and state of the widget.
193    /// It depend on the general widget style.
194    pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
195        let ws = self.widget_style(classes, state);
196        LabelStyle {
197            frame: Frame {
198                fill: ws.frame.fill,
199                inner_margin: 0.0.into(),
200                outer_margin: 0.0.into(),
201                stroke: Stroke::NONE,
202                shadow: Shadow::NONE,
203                corner_radius: 0.into(),
204            },
205            text: ws.text,
206            wrap_mode: TextWrapMode::Wrap,
207        }
208    }
209
210    /// The dedicated separator style. The style is computed according to the classes and state of the widget.
211    /// It depend on the general widget style.
212    pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
213        let visuals = self.visuals.noninteractive();
214        SeparatorStyle {
215            spacing: 6.0,
216            stroke: visuals.bg_stroke,
217        }
218    }
219}
220
221/// The root class is a special class present on every top-level [`crate::Ui`].
222pub const ROOT_CLASS: &str = "root";
223
224/// The selected class is a special class present on selected [`crate::Button`].
225pub const SELECTED_CLASS: &str = "selected";
226
227/// A class is a static string identifier.
228pub type ClassName = Cow<'static, str>;
229
230/// Classes are string identifier that can be set on widget/Ui.
231///
232/// This can be used by styling engine to compute a different style
233/// based on the set of classes present on the widget/Ui.
234#[derive(Debug, Default, Clone)]
235pub struct Classes {
236    classes: SmallVec<[ClassName; 5]>,
237}
238
239impl Classes {
240    /// Add a class to the list if the condition is true
241    #[inline]
242    fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
243        if condition {
244            self.classes.push(class.into());
245        }
246    }
247}
248
249impl HasClasses for Classes {
250    fn classes(&self) -> &Classes {
251        self
252    }
253
254    fn classes_mut(&mut self) -> &mut Classes {
255        self
256    }
257}
258
259impl core::fmt::Display for Classes {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        self.classes.iter().for_each(|class| {
262            let _ = f.write_str(class.as_str());
263        });
264        f.write_str("")
265    }
266}
267
268/// Any widgets supporting [`Classes`] must implement this trait
269pub trait HasClasses {
270    fn classes(&self) -> &Classes;
271
272    fn classes_mut(&mut self) -> &mut Classes;
273
274    /// Add the given class by consuming [`self`]
275    #[inline]
276    fn with_class(mut self, class: impl Into<ClassName>) -> Self
277    where
278        Self: Sized,
279    {
280        self.classes_mut().add_if(class.into(), true);
281        self
282    }
283
284    /// Add the given class by consuming [`self`] if the condition is true
285    #[inline]
286    fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
287    where
288        Self: Sized,
289    {
290        self.classes_mut().add_if(class.into(), condition);
291        self
292    }
293
294    /// Add the given class in-place
295    #[inline]
296    fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
297    where
298        Self: Sized,
299    {
300        self.classes_mut().add_if(class.into(), true);
301        self
302    }
303
304    /// Add the given class in-place if the condition is true
305    #[inline]
306    fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
307    where
308        Self: Sized,
309    {
310        self.classes_mut().add_if(class.into(), condition);
311        self
312    }
313
314    /// True if the class is present
315    fn has(&self, class: impl Into<ClassName>) -> bool {
316        self.classes().classes.contains(&class.into())
317    }
318}