egui/widgets/
button.rs

1use crate::{
2    widgets, Align, Color32, CornerRadius, Image, NumExt, Rect, Response, 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    corner_radius: Option<CornerRadius>,
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            corner_radius: 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 corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
157        self.corner_radius = Some(corner_radius.into());
158        self
159    }
160
161    #[inline]
162    #[deprecated = "Renamed to `corner_radius`"]
163    pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
164        self.corner_radius(corner_radius)
165    }
166
167    /// If true, the tint of the image is multiplied by the widget text color.
168    ///
169    /// This makes sense for images that are white, that should have the same color as the text color.
170    /// This will also make the icon color depend on hover state.
171    ///
172    /// Default: `false`.
173    #[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    /// Show some text on the right side of the button, in weak color.
180    ///
181    /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
182    ///
183    /// The text can be created with [`crate::Context::format_shortcut`].
184    #[inline]
185    pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
186        self.shortcut_text = shortcut_text.into();
187        self
188    }
189
190    /// If `true`, mark this button as "selected".
191    #[inline]
192    pub fn selected(mut self, selected: bool) -> Self {
193        self.selected = selected;
194        self
195    }
196}
197
198impl Widget for Button<'_> {
199    fn ui(self, ui: &mut Ui) -> Response {
200        let Button {
201            text,
202            image,
203            shortcut_text,
204            wrap_mode,
205            fill,
206            stroke,
207            sense,
208            small,
209            frame,
210            min_size,
211            corner_radius,
212            selected,
213            image_tint_follows_text_color,
214        } = self;
215
216        let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
217
218        let mut button_padding = if frame {
219            ui.spacing().button_padding
220        } else {
221            Vec2::ZERO
222        };
223        if small {
224            button_padding.y = 0.0;
225        }
226
227        let space_available_for_image = if let Some(text) = &text {
228            let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
229            Vec2::splat(font_height) // Reasonable?
230        } else {
231            ui.available_size() - 2.0 * button_padding
232        };
233
234        let image_size = if let Some(image) = &image {
235            image
236                .load_and_calc_size(ui, space_available_for_image)
237                .unwrap_or(space_available_for_image)
238        } else {
239            Vec2::ZERO
240        };
241
242        let gap_before_shortcut_text = ui.spacing().item_spacing.x;
243
244        let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
245        if image.is_some() {
246            text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
247        }
248
249        // Note: we don't wrap the shortcut text
250        let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
251            shortcut_text.into_galley(
252                ui,
253                Some(TextWrapMode::Extend),
254                f32::INFINITY,
255                TextStyle::Button,
256            )
257        });
258
259        if let Some(shortcut_galley) = &shortcut_galley {
260            // Leave space for the shortcut text:
261            text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
262        }
263
264        let galley =
265            text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
266
267        let mut desired_size = Vec2::ZERO;
268        if image.is_some() {
269            desired_size.x += image_size.x;
270            desired_size.y = desired_size.y.max(image_size.y);
271        }
272        if image.is_some() && galley.is_some() {
273            desired_size.x += ui.spacing().icon_spacing;
274        }
275        if let Some(galley) = &galley {
276            desired_size.x += galley.size().x;
277            desired_size.y = desired_size.y.max(galley.size().y);
278        }
279        if let Some(shortcut_galley) = &shortcut_galley {
280            desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
281            desired_size.y = desired_size.y.max(shortcut_galley.size().y);
282        }
283        desired_size += 2.0 * button_padding;
284        if !small {
285            desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
286        }
287        desired_size = desired_size.at_least(min_size);
288
289        let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
290        response.widget_info(|| {
291            if let Some(galley) = &galley {
292                WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
293            } else {
294                WidgetInfo::new(WidgetType::Button)
295            }
296        });
297
298        if ui.is_rect_visible(rect) {
299            let visuals = ui.style().interact(&response);
300
301            let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected {
302                let selection = ui.visuals().selection;
303                (
304                    Vec2::ZERO,
305                    CornerRadius::ZERO,
306                    selection.bg_fill,
307                    selection.stroke,
308                )
309            } else if frame {
310                let expansion = Vec2::splat(visuals.expansion);
311                (
312                    expansion,
313                    visuals.corner_radius,
314                    visuals.weak_bg_fill,
315                    visuals.bg_stroke,
316                )
317            } else {
318                Default::default()
319            };
320            let frame_cr = corner_radius.unwrap_or(frame_cr);
321            let frame_fill = fill.unwrap_or(frame_fill);
322            let frame_stroke = stroke.unwrap_or(frame_stroke);
323            ui.painter().rect(
324                rect.expand2(frame_expansion),
325                frame_cr,
326                frame_fill,
327                frame_stroke,
328                epaint::StrokeKind::Inside,
329            );
330
331            let mut cursor_x = rect.min.x + button_padding.x;
332
333            if let Some(image) = &image {
334                let mut image_pos = ui
335                    .layout()
336                    .align_size_within_rect(image_size, rect.shrink2(button_padding))
337                    .min;
338                if galley.is_some() || shortcut_galley.is_some() {
339                    image_pos.x = cursor_x;
340                }
341                let image_rect = Rect::from_min_size(image_pos, image_size);
342                cursor_x += image_size.x;
343                let tlr = image.load_for_size(ui.ctx(), image_size);
344                let mut image_options = image.image_options().clone();
345                if image_tint_follows_text_color {
346                    image_options.tint = image_options.tint * visuals.text_color();
347                }
348                widgets::image::paint_texture_load_result(
349                    ui,
350                    &tlr,
351                    image_rect,
352                    image.show_loading_spinner,
353                    &image_options,
354                    None,
355                );
356                response = widgets::image::texture_load_result_response(
357                    &image.source(ui.ctx()),
358                    &tlr,
359                    response,
360                );
361            }
362
363            if image.is_some() && galley.is_some() {
364                cursor_x += ui.spacing().icon_spacing;
365            }
366
367            if let Some(galley) = galley {
368                let mut text_pos = ui
369                    .layout()
370                    .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
371                    .min;
372                if image.is_some() || shortcut_galley.is_some() {
373                    text_pos.x = cursor_x;
374                }
375                ui.painter().galley(text_pos, galley, visuals.text_color());
376            }
377
378            if let Some(shortcut_galley) = shortcut_galley {
379                // Always align to the right
380                let layout = if ui.layout().is_horizontal() {
381                    ui.layout().with_main_align(Align::Max)
382                } else {
383                    ui.layout().with_cross_align(Align::Max)
384                };
385                let shortcut_text_pos = layout
386                    .align_size_within_rect(shortcut_galley.size(), rect.shrink2(button_padding))
387                    .min;
388                ui.painter().galley(
389                    shortcut_text_pos,
390                    shortcut_galley,
391                    ui.visuals().weak_text_color(),
392                );
393            }
394        }
395
396        if let Some(cursor) = ui.visuals().interact_cursor {
397            if response.hovered() {
398                ui.ctx().set_cursor_icon(cursor);
399            }
400        }
401
402        response
403    }
404}