egui/widgets/
button.rs

1use crate::{
2    widgets, Align, Color32, Image, NumExt, Rect, Response, Rounding, Sense, Stroke, TextStyle,
3    TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
4};
5
6/// Clickable button with text.
7///
8/// See also [`Ui::button`].
9///
10/// ```
11/// # egui::__run_test_ui(|ui| {
12/// # fn do_stuff() {}
13///
14/// if ui.add(egui::Button::new("Click me")).clicked() {
15///     do_stuff();
16/// }
17///
18/// // A greyed-out and non-interactive button:
19/// if ui.add_enabled(false, egui::Button::new("Can't click this")).clicked() {
20///     unreachable!();
21/// }
22/// # });
23/// ```
24#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
25pub struct Button<'a> {
26    image: Option<Image<'a>>,
27    text: Option<WidgetText>,
28    shortcut_text: WidgetText,
29    wrap_mode: Option<TextWrapMode>,
30
31    /// None means default for interact
32    fill: Option<Color32>,
33    stroke: Option<Stroke>,
34    sense: Sense,
35    small: bool,
36    frame: Option<bool>,
37    min_size: Vec2,
38    rounding: Option<Rounding>,
39    selected: bool,
40    image_tint_follows_text_color: bool,
41}
42
43impl<'a> Button<'a> {
44    pub fn new(text: impl Into<WidgetText>) -> Self {
45        Self::opt_image_and_text(None, Some(text.into()))
46    }
47
48    /// Creates a button with an image. The size of the image as displayed is defined by the provided size.
49    #[allow(clippy::needless_pass_by_value)]
50    pub fn image(image: impl Into<Image<'a>>) -> Self {
51        Self::opt_image_and_text(Some(image.into()), None)
52    }
53
54    /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
55    #[allow(clippy::needless_pass_by_value)]
56    pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
57        Self::opt_image_and_text(Some(image.into()), Some(text.into()))
58    }
59
60    pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
61        Self {
62            text,
63            image,
64            shortcut_text: Default::default(),
65            wrap_mode: None,
66            fill: None,
67            stroke: None,
68            sense: Sense::click(),
69            small: false,
70            frame: None,
71            min_size: Vec2::ZERO,
72            rounding: None,
73            selected: false,
74            image_tint_follows_text_color: false,
75        }
76    }
77
78    /// Set the wrap mode for the text.
79    ///
80    /// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`].
81    ///
82    /// Note that any `\n` in the text will always produce a new line.
83    #[inline]
84    pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
85        self.wrap_mode = Some(wrap_mode);
86        self
87    }
88
89    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`].
90    #[inline]
91    pub fn wrap(mut self) -> Self {
92        self.wrap_mode = Some(TextWrapMode::Wrap);
93
94        self
95    }
96
97    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`].
98    #[inline]
99    pub fn truncate(mut self) -> Self {
100        self.wrap_mode = Some(TextWrapMode::Truncate);
101        self
102    }
103
104    /// Override background fill color. Note that this will override any on-hover effects.
105    /// Calling this will also turn on the frame.
106    #[inline]
107    pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
108        self.fill = Some(fill.into());
109        self.frame = Some(true);
110        self
111    }
112
113    /// Override button stroke. Note that this will override any on-hover effects.
114    /// Calling this will also turn on the frame.
115    #[inline]
116    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
117        self.stroke = Some(stroke.into());
118        self.frame = Some(true);
119        self
120    }
121
122    /// Make this a small button, suitable for embedding into text.
123    #[inline]
124    pub fn small(mut self) -> Self {
125        if let Some(text) = self.text {
126            self.text = Some(text.text_style(TextStyle::Body));
127        }
128        self.small = true;
129        self
130    }
131
132    /// Turn off the frame
133    #[inline]
134    pub fn frame(mut self, frame: bool) -> Self {
135        self.frame = Some(frame);
136        self
137    }
138
139    /// By default, buttons senses clicks.
140    /// Change this to a drag-button with `Sense::drag()`.
141    #[inline]
142    pub fn sense(mut self, sense: Sense) -> Self {
143        self.sense = sense;
144        self
145    }
146
147    /// Set the minimum size of the button.
148    #[inline]
149    pub fn min_size(mut self, min_size: Vec2) -> Self {
150        self.min_size = min_size;
151        self
152    }
153
154    /// Set the rounding of the button.
155    #[inline]
156    pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
157        self.rounding = Some(rounding.into());
158        self
159    }
160
161    /// If true, the tint of the image is multiplied by the widget text color.
162    ///
163    /// This makes sense for images that are white, that should have the same color as the text color.
164    /// This will also make the icon color depend on hover state.
165    ///
166    /// Default: `false`.
167    #[inline]
168    pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
169        self.image_tint_follows_text_color = image_tint_follows_text_color;
170        self
171    }
172
173    /// Show some text on the right side of the button, in weak color.
174    ///
175    /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
176    ///
177    /// The text can be created with [`crate::Context::format_shortcut`].
178    #[inline]
179    pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
180        self.shortcut_text = shortcut_text.into();
181        self
182    }
183
184    /// If `true`, mark this button as "selected".
185    #[inline]
186    pub fn selected(mut self, selected: bool) -> Self {
187        self.selected = selected;
188        self
189    }
190}
191
192impl Widget for Button<'_> {
193    fn ui(self, ui: &mut Ui) -> Response {
194        let Button {
195            text,
196            image,
197            shortcut_text,
198            wrap_mode,
199            fill,
200            stroke,
201            sense,
202            small,
203            frame,
204            min_size,
205            rounding,
206            selected,
207            image_tint_follows_text_color,
208        } = self;
209
210        let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
211
212        let mut button_padding = if frame {
213            ui.spacing().button_padding
214        } else {
215            Vec2::ZERO
216        };
217        if small {
218            button_padding.y = 0.0;
219        }
220
221        let space_available_for_image = if let Some(text) = &text {
222            let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
223            Vec2::splat(font_height) // Reasonable?
224        } else {
225            ui.available_size() - 2.0 * button_padding
226        };
227
228        let image_size = if let Some(image) = &image {
229            image
230                .load_and_calc_size(ui, space_available_for_image)
231                .unwrap_or(space_available_for_image)
232        } else {
233            Vec2::ZERO
234        };
235
236        let gap_before_shortcut_text = ui.spacing().item_spacing.x;
237
238        let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
239        if image.is_some() {
240            text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
241        }
242
243        // Note: we don't wrap the shortcut text
244        let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
245            shortcut_text.into_galley(
246                ui,
247                Some(TextWrapMode::Extend),
248                f32::INFINITY,
249                TextStyle::Button,
250            )
251        });
252
253        if let Some(shortcut_galley) = &shortcut_galley {
254            // Leave space for the shortcut text:
255            text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
256        }
257
258        let galley =
259            text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
260
261        let mut desired_size = Vec2::ZERO;
262        if image.is_some() {
263            desired_size.x += image_size.x;
264            desired_size.y = desired_size.y.max(image_size.y);
265        }
266        if image.is_some() && galley.is_some() {
267            desired_size.x += ui.spacing().icon_spacing;
268        }
269        if let Some(galley) = &galley {
270            desired_size.x += galley.size().x;
271            desired_size.y = desired_size.y.max(galley.size().y);
272        }
273        if let Some(shortcut_galley) = &shortcut_galley {
274            desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
275            desired_size.y = desired_size.y.max(shortcut_galley.size().y);
276        }
277        desired_size += 2.0 * button_padding;
278        if !small {
279            desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
280        }
281        desired_size = desired_size.at_least(min_size);
282
283        let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
284        response.widget_info(|| {
285            if let Some(galley) = &galley {
286                WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
287            } else {
288                WidgetInfo::new(WidgetType::Button)
289            }
290        });
291
292        if ui.is_rect_visible(rect) {
293            let visuals = ui.style().interact(&response);
294
295            let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
296                let selection = ui.visuals().selection;
297                (
298                    Vec2::ZERO,
299                    Rounding::ZERO,
300                    selection.bg_fill,
301                    selection.stroke,
302                )
303            } else if frame {
304                let expansion = Vec2::splat(visuals.expansion);
305                (
306                    expansion,
307                    visuals.rounding,
308                    visuals.weak_bg_fill,
309                    visuals.bg_stroke,
310                )
311            } else {
312                Default::default()
313            };
314            let frame_rounding = rounding.unwrap_or(frame_rounding);
315            let frame_fill = fill.unwrap_or(frame_fill);
316            let frame_stroke = stroke.unwrap_or(frame_stroke);
317            ui.painter().rect(
318                rect.expand2(frame_expansion),
319                frame_rounding,
320                frame_fill,
321                frame_stroke,
322            );
323
324            let mut cursor_x = rect.min.x + button_padding.x;
325
326            if let Some(image) = &image {
327                let mut image_pos = ui
328                    .layout()
329                    .align_size_within_rect(image_size, rect.shrink2(button_padding))
330                    .min;
331                if galley.is_some() || shortcut_galley.is_some() {
332                    image_pos.x = cursor_x;
333                }
334                let image_rect = Rect::from_min_size(image_pos, image_size);
335                cursor_x += image_size.x;
336                let tlr = image.load_for_size(ui.ctx(), image_size);
337                let mut image_options = image.image_options().clone();
338                if image_tint_follows_text_color {
339                    image_options.tint = image_options.tint * visuals.text_color();
340                }
341                widgets::image::paint_texture_load_result(
342                    ui,
343                    &tlr,
344                    image_rect,
345                    image.show_loading_spinner,
346                    &image_options,
347                );
348                response = widgets::image::texture_load_result_response(
349                    &image.source(ui.ctx()),
350                    &tlr,
351                    response,
352                );
353            }
354
355            if image.is_some() && galley.is_some() {
356                cursor_x += ui.spacing().icon_spacing;
357            }
358
359            if let Some(galley) = galley {
360                let mut text_pos = ui
361                    .layout()
362                    .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
363                    .min;
364                if image.is_some() || shortcut_galley.is_some() {
365                    text_pos.x = cursor_x;
366                }
367                ui.painter().galley(text_pos, galley, visuals.text_color());
368            }
369
370            if let Some(shortcut_galley) = shortcut_galley {
371                // Always align to the right
372                let layout = if ui.layout().is_horizontal() {
373                    ui.layout().with_main_align(Align::Max)
374                } else {
375                    ui.layout().with_cross_align(Align::Max)
376                };
377                let shortcut_text_pos = layout
378                    .align_size_within_rect(shortcut_galley.size(), rect.shrink2(button_padding))
379                    .min;
380                ui.painter().galley(
381                    shortcut_text_pos,
382                    shortcut_galley,
383                    ui.visuals().weak_text_color(),
384                );
385            }
386        }
387
388        if let Some(cursor) = ui.visuals().interact_cursor {
389            if response.hovered {
390                ui.ctx().set_cursor_icon(cursor);
391            }
392        }
393
394        response
395    }
396}