egui/widgets/
button.rs

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