egui_charts/ui_kit/list_item/
button_content.rs1use crate::localization as l10n;
4use egui::{Color32, Response, Role, Ui, Vec2, WidgetInfo};
5
6use crate::icons::Icon;
7use crate::theme::ThemeExt;
8use crate::tokens::DESIGN_TOKENS;
9use crate::ui_kit::buttons::ButtonVariant;
10
11use super::content::{ContentContext, ListItemContent};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum ButtonStyle {
16 #[default]
18 Default,
19 Primary,
21 Danger,
23 Subtle,
25}
26
27pub struct ButtonContent<'a> {
48 icon: &'a Icon,
49 label: Option<String>,
50 style: ButtonStyle,
51 icon_size: f32,
52 disabled: bool,
53}
54
55impl<'a> ButtonContent<'a> {
56 pub fn new(icon: &'a Icon, label: impl Into<String>) -> Self {
58 Self {
59 icon,
60 label: Some(label.into()),
61 style: ButtonStyle::Default,
62 icon_size: DESIGN_TOKENS.sizing.icon_sm,
63 disabled: false,
64 }
65 }
66
67 pub fn icon_only(icon: &'a Icon) -> Self {
69 Self {
70 icon,
71 label: None,
72 style: ButtonStyle::Default,
73 icon_size: DESIGN_TOKENS.sizing.icon_sm,
74 disabled: false,
75 }
76 }
77
78 #[must_use]
80 pub fn style(mut self, style: ButtonStyle) -> Self {
81 self.style = style;
82 self
83 }
84
85 #[must_use]
87 pub fn primary(mut self) -> Self {
88 self.style = ButtonStyle::Primary;
89 self
90 }
91
92 #[must_use]
94 pub fn danger(mut self) -> Self {
95 self.style = ButtonStyle::Danger;
96 self
97 }
98
99 #[must_use]
101 pub fn subtle(mut self) -> Self {
102 self.style = ButtonStyle::Subtle;
103 self
104 }
105
106 #[must_use]
108 pub fn icon_size(mut self, size: f32) -> Self {
109 self.icon_size = size;
110 self
111 }
112
113 #[must_use]
115 pub fn disabled(mut self, disabled: bool) -> Self {
116 self.disabled = disabled;
117 self
118 }
119
120 fn get_colors(&self, ui: &Ui, hovered: bool, active: bool) -> (Color32, Color32, Color32) {
122 let visuals = &ui.style().visuals;
123
124 if self.disabled || !ui.is_enabled() {
125 let text = ui.active_theme().ui.text_disabled;
126 return (Color32::TRANSPARENT, text, text);
127 }
128
129 match self.style {
130 ButtonStyle::Default => {
131 if active {
132 (
133 visuals.widgets.active.bg_fill,
134 visuals.widgets.active.fg_stroke.color,
135 visuals.widgets.active.fg_stroke.color,
136 )
137 } else if hovered {
138 (
139 visuals.widgets.hovered.bg_fill,
140 visuals.widgets.hovered.fg_stroke.color,
141 visuals.widgets.hovered.fg_stroke.color,
142 )
143 } else {
144 (
145 Color32::TRANSPARENT,
146 visuals.widgets.inactive.fg_stroke.color,
147 visuals.widgets.inactive.fg_stroke.color,
148 )
149 }
150 }
151 ButtonStyle::Primary | ButtonStyle::Danger => {
152 let variant = if self.style == ButtonStyle::Primary {
153 ButtonVariant::Primary
154 } else {
155 ButtonVariant::Danger
156 };
157 if active || hovered {
158 let text = variant.fg_color(ui);
159 (variant.bg_color(ui, hovered, active), text, text)
160 } else {
161 let text = if self.style == ButtonStyle::Danger {
162 ui.active_theme().ui.error
163 } else {
164 visuals.text_color()
165 };
166 (Color32::TRANSPARENT, text, text)
167 }
168 }
169 ButtonStyle::Subtle => {
170 if active {
171 (
172 visuals.widgets.active.bg_fill.gamma_multiply(0.5),
173 visuals.widgets.active.fg_stroke.color,
174 visuals.widgets.active.fg_stroke.color,
175 )
176 } else if hovered {
177 (
178 visuals.widgets.hovered.bg_fill.gamma_multiply(0.5),
179 visuals.widgets.hovered.fg_stroke.color,
180 visuals.widgets.hovered.fg_stroke.color,
181 )
182 } else {
183 (
184 Color32::TRANSPARENT,
185 visuals.widgets.noninteractive.fg_stroke.color,
186 visuals.widgets.noninteractive.fg_stroke.color,
187 )
188 }
189 }
190 }
191 }
192}
193
194impl<'a> ListItemContent for ButtonContent<'a> {
195 fn ui(self, ui: &mut Ui, context: &ContentContext<'_>) -> Response {
196 let response = context.response;
197 let translated_label = l10n::text(ui.ctx(), self.label.as_deref().unwrap_or_default());
198 response.widget_info(|| {
199 WidgetInfo::labeled(
200 Role::Button,
201 !self.disabled && ui.is_enabled(),
202 &translated_label,
203 )
204 });
205
206 if ui.is_rect_visible(response.rect) {
207 let (bg_color, icon_color, text_color) =
208 self.get_colors(ui, response.hovered(), response.is_pointer_button_down_on());
209
210 if bg_color != Color32::TRANSPARENT {
212 ui.painter()
213 .rect_filled(response.rect, ui.active_theme().radius.sm, bg_color);
214 }
215
216 let content_rect = context.rect;
217
218 let icon_rect = egui::Rect::from_min_size(
220 content_rect.min + Vec2::new(0.0, (content_rect.height() - self.icon_size) / 2.0),
221 Vec2::splat(self.icon_size),
222 );
223 self.icon
224 .as_image_tinted(Vec2::splat(self.icon_size), icon_color)
225 .paint_at(ui, icon_rect);
226
227 if let Some(label) = &self.label {
229 let text_pos = egui::pos2(
230 content_rect.min.x + self.icon_size + ui.active_theme().spacing.sm,
231 content_rect.center().y,
232 );
233
234 ui.painter().text(
235 text_pos,
236 egui::Align2::LEFT_CENTER,
237 l10n::text(ui.ctx(), label),
238 egui::TextStyle::Body.resolve(ui.style()),
239 text_color,
240 );
241 }
242 }
243
244 response.clone()
245 }
246
247 fn interactive(&self) -> bool {
248 !self.disabled
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::icons::icons;
256 use crate::theme::{apply_to_egui, contrast, presets};
257
258 #[test]
259 fn action_rows_are_transparent_at_rest_and_use_readable_state_colours() {
260 for (name, theme) in presets() {
261 let ctx = egui::Context::default();
262 apply_to_egui(&ctx, &theme);
263 ctx.run_ui(Default::default(), |ui| {
264 for style in [
265 ButtonStyle::Default,
266 ButtonStyle::Primary,
267 ButtonStyle::Danger,
268 ButtonStyle::Subtle,
269 ] {
270 let row = ButtonContent::new(&icons::PLUS, "Action").style(style);
271 let (fill, icon, text) = row.get_colors(ui, false, false);
272 assert_eq!(fill, Color32::TRANSPARENT, "{name}: {style:?}");
273 assert_eq!(icon, text);
274 assert!(
275 contrast(text, theme.ui.panel_bg) >= 4.5,
276 "{name}: {style:?}"
277 );
278 let disabled = row.disabled(true).get_colors(ui, true, true);
279 assert_eq!(
280 disabled,
281 (
282 Color32::TRANSPARENT,
283 theme.ui.text_disabled,
284 theme.ui.text_disabled
285 )
286 );
287 }
288 for (row, hover, active, text) in [
289 (
290 ButtonContent::new(&icons::PLUS, "Add").primary(),
291 theme.ui.accent_hover,
292 theme.ui.accent_active,
293 theme.ui.accent_text,
294 ),
295 (
296 ButtonContent::new(&icons::CLOSE, "Delete").danger(),
297 theme.ui.danger_bg_hover,
298 theme.ui.danger_bg_active,
299 theme.ui.danger_text,
300 ),
301 ] {
302 assert_eq!(row.get_colors(ui, true, false), (hover, text, text));
303 assert_eq!(row.get_colors(ui, true, true), (active, text, text));
304 }
305 })
306 .drop_without_applying_deltas();
307 }
308 }
309}