Skip to main content

egui/widget_style/
mod.rs

1// This module is only public with the `experimental_theme` feature,
2// so without it a lot of it looks unused:
3#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
4
5use core::fmt::Debug;
6
7use emath::{Align2, Vec2};
8use epaint::{Color32, FontId, Stroke};
9
10use crate::{
11    Context, FontSelection, Frame, Response, Style, UiStack, WidgetAtom,
12    class::{Classes, HasClasses as _},
13    style::{WidgetVisuals, Widgets},
14};
15
16/// Each dedicated style must implement this trait to be used in the theme plugin system
17pub trait WidgetStyle: Debug + Clone + Send + Sync + core::any::Any + 'static {}
18
19/// General text style
20#[derive(Debug, Clone)]
21pub struct TextVisuals {
22    /// Font used
23    pub font_id: FontId,
24
25    /// Font color
26    pub color: Color32,
27}
28
29impl TextVisuals {
30    /// Text in `color`, using the given font.
31    ///
32    /// `style.override_font_id` wins over `font`, if it is set.
33    pub fn new(style: &Style, font: impl Into<FontSelection>, color: Color32) -> Self {
34        Self {
35            color,
36            font_id: style
37                .override_font_id
38                .clone()
39                .unwrap_or_else(|| font.into().resolve(style)),
40        }
41    }
42
43    /// The text of a widget, colored by the [`WidgetVisuals`] of its current state.
44    pub fn from_widget_visuals(
45        style: &Style,
46        font: impl Into<FontSelection>,
47        widget_visuals: &WidgetVisuals,
48    ) -> Self {
49        Self::new(style, font, widget_visuals.text_color())
50    }
51}
52
53/// Visual and layout style shared by widgets built from a [`WidgetAtom`].
54#[derive(Debug, Clone)]
55pub struct AtomLayoutStyle {
56    /// Alignment of the atoms within the allocated rectangle.
57    ///
58    /// `None` uses the alignment of the surrounding [`crate::Ui`].
59    pub align2: Option<Align2>,
60
61    /// Minimum size of the atom layout.
62    pub min_size: Vec2,
63
64    /// Space between adjacent atoms.
65    pub gap: f32,
66
67    /// Frame around the atoms.
68    pub frame: Frame,
69
70    /// Fallback visuals for text atoms.
71    pub text_style: TextVisuals,
72
73    /// Fallback tint for images whose tint is [`Color32::WHITE`] (untinted).
74    pub image_tint: Color32,
75}
76
77impl Default for AtomLayoutStyle {
78    fn default() -> Self {
79        Self {
80            align2: None,
81            min_size: Vec2::ZERO,
82            gap: 0.0,
83            frame: Frame::default(),
84            text_style: TextVisuals {
85                font_id: FontId::default(),
86                color: Color32::WHITE,
87            },
88            image_tint: Color32::WHITE,
89        }
90    }
91}
92
93impl AtomLayoutStyle {
94    /// Apply this style to a [`WidgetAtom`].
95    ///
96    /// A per-widget [`WidgetAtom::gap`] wins over [`Self::gap`], so widgets like
97    /// [`crate::DragValue`] can pack their atoms tighter than the theme does.
98    pub fn apply(self, mut layout: WidgetAtom<'_>) -> WidgetAtom<'_> {
99        let Self {
100            align2,
101            min_size,
102            gap,
103            frame,
104            text_style,
105            image_tint,
106        } = self;
107        layout.map_images(|image| {
108            let current_tint = image.image_options().tint;
109            // Multiply the tints so they are combined
110            image.tint(current_tint * image_tint)
111        });
112
113        let layout = layout
114            .min_size(min_size)
115            .fallback_gap(gap)
116            .frame(frame)
117            .fallback_font(text_style.font_id)
118            .fallback_text_color(text_style.color);
119
120        if let Some(align2) = align2 {
121            layout.align2(align2)
122        } else {
123            layout
124        }
125    }
126}
127
128/// Dedicated button style
129#[derive(Debug, Clone)]
130pub struct ButtonStyle {
131    pub atom_layout: AtomLayoutStyle,
132}
133
134impl WidgetStyle for ButtonStyle {}
135
136/// Dedicated text edit style
137#[derive(Debug, Clone)]
138pub struct TextEditStyle {
139    /// Style of the field's atom layout.
140    ///
141    /// [`AtomLayoutStyle::frame`] surrounds the text, including its padding, and
142    /// [`AtomLayoutStyle::text_style`] is the text being edited.
143    pub atom_layout: AtomLayoutStyle,
144
145    /// The color of the hint text shown while the buffer is empty.
146    pub hint_text_color: Color32,
147
148    /// The default color of the prefix and suffix atoms.
149    pub prefix_suffix_color: Color32,
150}
151
152impl WidgetStyle for TextEditStyle {}
153
154/// Dedicated checkbox style
155#[derive(Debug, Clone)]
156pub struct CheckboxStyle {
157    /// Style of the checkbox's atom layout.
158    ///
159    /// [`AtomLayoutStyle::align2`] vertical align has no effect; the checkbox is always centered.
160    pub atom_layout: AtomLayoutStyle,
161
162    /// Checkbox size
163    pub checkbox_size: f32,
164
165    /// Checkmark size
166    pub check_size: f32,
167
168    /// Frame of the checkbox itself
169    pub checkbox_frame: Frame,
170
171    /// Checkmark stroke
172    pub check_stroke: Stroke,
173}
174
175impl WidgetStyle for CheckboxStyle {}
176
177/// Dedicated separator style
178#[derive(Debug, Clone)]
179pub struct SeparatorStyle {
180    /// How much space is allocated in the layout direction
181    pub spacing: f32,
182
183    /// How to paint it
184    pub stroke: Stroke,
185}
186
187impl WidgetStyle for SeparatorStyle {}
188
189/// The different state of a widget can be
190#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
191pub enum WidgetState {
192    Noninteractive,
193    #[default]
194    Inactive,
195    Hovered,
196    Active,
197}
198
199impl Widgets {
200    /// The widget visuals according to the state
201    pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
202        match state {
203            WidgetState::Noninteractive => &self.noninteractive,
204            WidgetState::Inactive => &self.inactive,
205            WidgetState::Hovered => &self.hovered,
206            WidgetState::Active => &self.active,
207        }
208    }
209}
210
211impl Response {
212    pub fn widget_state(&self) -> WidgetState {
213        if !self.sense.interactive() {
214            WidgetState::Noninteractive
215        } else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
216            WidgetState::Active
217        } else if self.hovered() || self.highlighted() {
218            WidgetState::Hovered
219        } else {
220            WidgetState::Inactive
221        }
222    }
223}
224
225pub struct StyleArgs<'a> {
226    pub classes: &'a Classes,
227    pub state: WidgetState,
228    pub stack: &'a UiStack,
229    pub style: &'a Style,
230    pub ctx: &'a Context,
231}
232
233impl StyleArgs<'_> {
234    /// Does the widget or any of its parents contain this class?
235    ///
236    /// See also:
237    /// - [`Classes::has_class`]
238    /// - [`UiStack::has_class`]
239    pub fn has_class(&self, class: &str) -> bool {
240        self.classes.has_class(class) || self.stack.has_class(class)
241    }
242}