egui_charts/ui_kit/buttons/
variant.rs1use egui::{Color32, Ui};
7
8use crate::theme::ThemeExt;
9use crate::tokens::DESIGN_TOKENS;
10
11#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
15pub enum ButtonVariant {
16 Primary,
20
21 Secondary,
25
26 #[default]
30 Ghost,
31
32 Outlined,
37
38 Danger,
42}
43
44impl ButtonVariant {
45 pub fn bg_color(&self, ui: &Ui, hovered: bool, pressed: bool) -> Color32 {
52 let state = |rest, hover, press| {
53 if pressed {
54 press
55 } else if hovered {
56 hover
57 } else {
58 rest
59 }
60 };
61 match self {
62 Self::Primary if ui.is_enabled() => {
63 let colors = ui.active_theme().ui;
64 return state(colors.accent, colors.accent_hover, colors.accent_active);
65 }
66 Self::Danger if ui.is_enabled() => {
67 let colors = ui.active_theme().ui;
68 return state(
69 colors.danger_bg,
70 colors.danger_bg_hover,
71 colors.danger_bg_active,
72 );
73 }
74 Self::Ghost | Self::Outlined => {
75 return state(
76 Color32::TRANSPARENT,
77 ui.visuals().widgets.hovered.bg_fill,
78 ui.visuals().widgets.active.bg_fill,
79 );
80 }
81 Self::Secondary | Self::Primary | Self::Danger => {}
82 }
83 let steps = 1 + u8::from(hovered || pressed) + u8::from(pressed);
84 Color32::TRANSPARENT.lerp_to_gamma(ui.visuals().strong_text_color(), 0.1 * f32::from(steps))
85 }
86
87 pub fn fg_color(&self, ui: &Ui) -> Color32 {
89 match self {
90 Self::Primary if ui.is_enabled() => ui.active_theme().ui.accent_text,
91 Self::Danger if ui.is_enabled() => ui.active_theme().ui.danger_text,
92 _ => ui.visuals().widgets.inactive.fg_stroke.color,
93 }
94 }
95
96 pub fn border_color(&self, ui: &Ui) -> Option<Color32> {
99 match self {
100 Self::Outlined => Some(ui.visuals().widgets.inactive.fg_stroke.color),
101 Self::Primary if !ui.is_enabled() => Some(ui.active_theme().ui.accent),
102 _ => None,
103 }
104 }
105
106 pub fn apply_to_style(&self, ui: &mut Ui, hovered: bool, active: bool) {
110 let bg = self.bg_color(ui, hovered, active);
111 let fg = if self.is_inverse() {
112 self.fg_color(ui)
113 } else if active {
114 ui.visuals().widgets.active.fg_stroke.color
115 } else if hovered {
116 ui.visuals().widgets.hovered.fg_stroke.color
117 } else {
118 self.fg_color(ui)
119 };
120 let border = self.border_color(ui);
121
122 let widgets = &mut ui.visuals_mut().widgets;
123 for w in [
124 &mut widgets.inactive,
125 &mut widgets.hovered,
126 &mut widgets.active,
127 ] {
128 w.bg_fill = bg;
129 w.weak_bg_fill = bg;
130 w.fg_stroke.color = fg;
131 if let Some(border) = border {
132 w.bg_stroke = egui::Stroke::new(DESIGN_TOKENS.stroke.hairline, border);
133 }
134 }
135 }
136
137 pub fn is_inverse(&self) -> bool {
139 matches!(self, Self::Primary | Self::Danger)
140 }
141
142 pub fn has_background(&self) -> bool {
144 !matches!(self, Self::Ghost | Self::Outlined)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::theme::{Theme, apply_to_egui, contrast, presets};
152
153 #[test]
154 fn neutral_buttons_share_widget_fills_and_resting_foreground() {
155 for (_, theme) in presets() {
156 let ctx = egui::Context::default();
157 apply_to_egui(&ctx, &theme);
158 ctx.run_ui(Default::default(), |ui| {
159 ui.visuals_mut().widgets.inactive.fg_stroke.color =
161 Color32::from_rgb(150, 160, 170);
162 for variant in [ButtonVariant::Ghost, ButtonVariant::Outlined] {
163 assert_eq!(
164 variant.fg_color(ui),
165 ui.visuals().widgets.inactive.fg_stroke.color
166 );
167 assert_eq!(variant.bg_color(ui, false, false), Color32::TRANSPARENT);
168 for (hovered, pressed, expected) in [
169 (true, false, ui.visuals().widgets.hovered.bg_fill),
170 (false, true, ui.visuals().widgets.active.bg_fill),
171 (true, true, ui.visuals().widgets.active.bg_fill),
172 ] {
173 assert_eq!(variant.bg_color(ui, hovered, pressed), expected);
174 }
175 }
176 })
177 .drop_without_applying_deltas();
178 }
179 }
180
181 #[test]
182 fn primary_and_danger_text_reads_on_the_fill_in_every_state_and_preset() {
183 let mut failures = Vec::new();
184 let orange = Theme::light().with_accent(Color32::from_rgb(255, 120, 0));
186 for (name, theme) in presets()
187 .into_iter()
188 .chain([("light, orange accent", orange)])
189 {
190 let ctx = egui::Context::default();
191 apply_to_egui(&ctx, &theme);
192 ctx.run_ui(Default::default(), |ui| {
193 egui::CentralPanel::default().show(ui, |ui| {
194 for (enabled, min) in [(true, 4.5), (false, 3.0)] {
197 ui.add_enabled_ui(enabled, |ui| {
198 let fade = ui.painter().opacity();
199 let surface = ui.visuals().panel_fill;
200 for variant in [ButtonVariant::Primary, ButtonVariant::Danger] {
201 let text = variant.fg_color(ui).gamma_multiply(fade);
202 for (state, hovered, pressed) in [
203 ("rest", false, false),
204 ("hovered", true, false),
205 ("pressed", true, true),
206 ] {
207 let fill = variant.bg_color(ui, hovered, pressed);
208 let ratio =
209 contrast(text, surface.blend(fill.gamma_multiply(fade)));
210 if ratio < min {
211 failures.push(format!(
212 "{name}: {variant:?} {state} (enabled: {enabled}) is \
213 {ratio:.2}:1, below {min}:1"
214 ));
215 }
216 }
217 }
218 });
219 }
220 });
221 })
222 .drop_without_applying_deltas();
223 }
224 assert!(failures.is_empty(), "{}", failures.join("\n"));
225 }
226}