1use egui::{
2 Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, Frame, Image, IntoAtoms,
3 NumExt as _, Response, Sense, Shadow, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo,
4 WidgetText, WidgetType,
5};
6
7use crate::theme::Theme;
8use crate::visuals::ButtonVisuals;
9use crate::widgets::badge::Badge;
10
11#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
28pub struct Button<'a> {
29 layout: AtomLayout<'a>,
30 visuals: Option<ButtonVisuals>,
31 bg_color: Option<Color32>,
32 badge: Option<Badge>,
33 small: bool,
34 frame_when_inactive: bool,
35 min_size: Vec2,
36 selected: bool,
37 image_tint_follows_text_color: bool,
38 limit_image_size: bool,
39}
40
41impl<'a> Button<'a> {
42 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
44 Self {
45 layout: AtomLayout::new(atoms.into_atoms())
46 .sense(Sense::click())
47 .fallback_font(TextStyle::Button),
48 visuals: None,
49 bg_color: None,
50 badge: None,
51 small: false,
52 frame_when_inactive: true,
53 min_size: Vec2::ZERO,
54 selected: false,
55 image_tint_follows_text_color: false,
56 limit_image_size: false,
57 }
58 }
59
60 pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
75 Self::new(atoms).selected(selected).frame_when_inactive(selected)
76 }
77
78 pub fn image(image: impl Into<Image<'a>>) -> Self {
83 Self::opt_image_and_text(Some(image.into()), None)
84 }
85
86 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
91 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
92 }
93
94 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
99 let mut button = Self::new(());
100 if let Some(image) = image {
101 button.layout.push_right(image);
102 }
103 if let Some(text) = text {
104 button.layout.push_right(text);
105 }
106 button.limit_image_size = true;
107 button
108 }
109
110 #[inline]
116 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
117 self.layout = self.layout.wrap_mode(wrap_mode);
118 self
119 }
120
121 #[inline]
123 pub fn wrap(self) -> Self {
124 self.wrap_mode(TextWrapMode::Wrap)
125 }
126
127 #[inline]
129 pub fn truncate(self) -> Self {
130 self.wrap_mode(TextWrapMode::Truncate)
131 }
132
133 #[inline]
135 pub fn small(mut self) -> Self {
136 self.small = true;
137 self
138 }
139
140 #[inline]
147 pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
148 self.frame_when_inactive = frame_when_inactive;
149 self
150 }
151
152 #[inline]
155 pub fn sense(mut self, sense: Sense) -> Self {
156 self.layout = self.layout.sense(sense);
157 self
158 }
159
160 #[inline]
162 pub fn min_size(mut self, min_size: Vec2) -> Self {
163 self.min_size = min_size;
164 self
165 }
166
167 #[inline]
174 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
175 self.image_tint_follows_text_color = image_tint_follows_text_color;
176 self
177 }
178
179 #[inline]
187 pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
188 let mut atom = shortcut_text.into();
189 atom.kind = match atom.kind {
190 AtomKind::Text(text) => AtomKind::Text(text.weak()),
191 other => other,
192 };
193 self.layout.push_right(Atom::grow());
194 self.layout.push_right(atom);
195 self
196 }
197
198 #[inline]
200 pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
201 self.layout.push_right(Atom::grow());
202 self.layout.push_right(right_text.into());
203 self
204 }
205
206 #[inline]
208 pub fn selected(mut self, selected: bool) -> Self {
209 self.selected = selected;
210 self
211 }
212
213 #[inline]
215 pub fn visuals(mut self, visuals: ButtonVisuals) -> Self {
216 self.visuals = Some(visuals);
217 self
218 }
219
220 #[inline]
222 pub fn bg_color(mut self, color: Color32) -> Self {
223 self.bg_color = Some(color);
224 self
225 }
226
227 #[inline]
248 pub fn badge(mut self, badge: Badge) -> Self {
249 self.badge = Some(badge);
250 self
251 }
252
253 pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
255 let Button {
256 mut layout,
257 bg_color,
258 badge,
259 small,
260 visuals,
261 frame_when_inactive,
262 mut min_size,
263 selected,
264 image_tint_follows_text_color,
265 limit_image_size,
266 } = self;
267
268 let visuals = visuals.or_else(|| Theme::button_visuals_from_ctx(ui.ctx()));
269
270 if !small {
271 min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
272 }
273 if limit_image_size {
274 layout.map_atoms(|atom| {
275 if matches!(&atom.kind, AtomKind::Image(_)) {
276 atom.atom_max_height_font_size(ui)
277 } else {
278 atom
279 }
280 });
281 }
282
283 let text = layout.text().map(String::from);
284
285 let has_frame_margin = visuals.is_some() || ui.visuals().button_frame;
286 let mut button_padding = if has_frame_margin {
287 ui.spacing().button_padding
288 } else {
289 Vec2::ZERO
290 };
291
292 if small {
293 button_padding.y = 0.0;
294 }
295
296 let frame = Frame::new().inner_margin(button_padding);
297 let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
298
299 let response = if ui.is_rect_visible(prepared.response.rect) {
300 let interact_visuals = ui.style().interact_selectable(&prepared.response, selected);
301
302 let is_active = prepared.response.is_pointer_button_down_on(); let is_hovered = prepared.response.hovered();
304
305 let (fill, stroke, corner_radius) = if let Some(v) = &visuals {
307 if is_active {
308 (v.bg_click, v.border_click, v.corner_radius)
309 } else if is_hovered {
310 (v.bg_hover, v.border_hover, v.corner_radius)
311 } else {
312 let bg = bg_color.unwrap_or(v.bg);
313 (bg, v.border, v.corner_radius)
314 }
315 } else {
316 (
317 interact_visuals.weak_bg_fill,
318 interact_visuals.bg_stroke,
319 interact_visuals.corner_radius,
320 )
321 };
322
323 let fill = match selected {
324 false => fill,
325 true => {
326 visuals.as_ref().map(|v| v.bg_selected).unwrap_or(interact_visuals.weak_bg_fill)
327 }
328 };
329
330 let text_color = visuals.as_ref().map(|v| v.text).unwrap_or(interact_visuals.text_color());
331
332 let visible_frame = if frame_when_inactive {
333 has_frame_margin
334 } else {
335 has_frame_margin && (is_hovered || is_active || prepared.response.has_focus())
336 };
337
338 if image_tint_follows_text_color {
339 prepared.map_images(|image| image.tint(text_color));
340 }
341 prepared.fallback_text_color = text_color;
342
343 if visible_frame {
344 let shadow = visuals.as_ref().map(|v| v.shadow).unwrap_or(Shadow::NONE);
345 prepared.frame = prepared
346 .frame
347 .inner_margin(
348 button_padding + Vec2::splat(interact_visuals.expansion)
349 - Vec2::splat(stroke.width),
350 )
351 .outer_margin(-Vec2::splat(interact_visuals.expansion))
352 .fill(fill)
353 .stroke(stroke)
354 .corner_radius(corner_radius)
355 .shadow(shadow);
356 }
357
358 let painted = prepared.paint(ui);
359 if let Some(badge) = &badge {
360 badge.paint_at(ui, painted.response.rect);
361 }
362 painted
363 } else {
364 AtomLayoutResponse::empty(prepared.response)
365 };
366
367 response.response.widget_info(|| {
368 if let Some(text) = &text {
369 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
370 } else {
371 WidgetInfo::new(WidgetType::Button)
372 }
373 });
374
375 response
376 }
377}
378
379impl Widget for Button<'_> {
380 fn ui(self, ui: &mut Ui) -> Response {
381 self.atom_ui(ui).response
382 }
383}