egui/widgets/
button.rs

1use crate::{
2    Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
3    Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2,
4    Widget, WidgetInfo, WidgetText, WidgetType,
5};
6
7/// Clickable button with text.
8///
9/// See also [`Ui::button`].
10///
11/// ```
12/// # egui::__run_test_ui(|ui| {
13/// # fn do_stuff() {}
14///
15/// if ui.add(egui::Button::new("Click me")).clicked() {
16///     do_stuff();
17/// }
18///
19/// // A greyed-out and non-interactive button:
20/// if ui.add_enabled(false, egui::Button::new("Can't click this")).clicked() {
21///     unreachable!();
22/// }
23/// # });
24/// ```
25#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
26pub struct Button<'a> {
27    layout: AtomLayout<'a>,
28    fill: Option<Color32>,
29    stroke: Option<Stroke>,
30    small: bool,
31    frame: Option<bool>,
32    frame_when_inactive: bool,
33    min_size: Vec2,
34    corner_radius: Option<CornerRadius>,
35    selected: bool,
36    image_tint_follows_text_color: bool,
37    limit_image_size: bool,
38}
39
40impl<'a> Button<'a> {
41    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
42        Self {
43            layout: AtomLayout::new(atoms.into_atoms())
44                .sense(Sense::click())
45                .fallback_font(TextStyle::Button),
46            fill: None,
47            stroke: None,
48            small: false,
49            frame: None,
50            frame_when_inactive: true,
51            min_size: Vec2::ZERO,
52            corner_radius: None,
53            selected: false,
54            image_tint_follows_text_color: false,
55            limit_image_size: false,
56        }
57    }
58
59    /// Show a selectable button.
60    ///
61    /// Equivalent to:
62    /// ```rust
63    /// # use egui::{Button, IntoAtoms, __run_test_ui};
64    /// # __run_test_ui(|ui| {
65    /// let selected = true;
66    /// ui.add(Button::new("toggle me").selected(selected).frame_when_inactive(!selected).frame(true));
67    /// # });
68    /// ```
69    ///
70    /// See also:
71    ///   - [`Ui::selectable_value`]
72    ///   - [`Ui::selectable_label`]
73    pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
74        Self::new(atoms)
75            .selected(selected)
76            .frame_when_inactive(selected)
77            .frame(true)
78    }
79
80    /// Creates a button with an image. The size of the image as displayed is defined by the provided size.
81    ///
82    /// Note: In contrast to [`Button::new`], this limits the image size to the default font height
83    /// (using [`crate::AtomExt::atom_max_height_font_size`]).
84    pub fn image(image: impl Into<Image<'a>>) -> Self {
85        Self::opt_image_and_text(Some(image.into()), None)
86    }
87
88    /// Creates a button with an image to the left of the text.
89    ///
90    /// Note: In contrast to [`Button::new`], this limits the image size to the default font height
91    /// (using [`crate::AtomExt::atom_max_height_font_size`]).
92    pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
93        Self::opt_image_and_text(Some(image.into()), Some(text.into()))
94    }
95
96    /// Create a button with an optional image and optional text.
97    ///
98    /// Note: In contrast to [`Button::new`], this limits the image size to the default font height
99    /// (using [`crate::AtomExt::atom_max_height_font_size`]).
100    pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
101        let mut button = Self::new(());
102        if let Some(image) = image {
103            button.layout.push_right(image);
104        }
105        if let Some(text) = text {
106            button.layout.push_right(text);
107        }
108        button.limit_image_size = true;
109        button
110    }
111
112    /// Set the wrap mode for the text.
113    ///
114    /// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`].
115    ///
116    /// Note that any `\n` in the text will always produce a new line.
117    #[inline]
118    pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
119        self.layout = self.layout.wrap_mode(wrap_mode);
120        self
121    }
122
123    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`].
124    #[inline]
125    pub fn wrap(self) -> Self {
126        self.wrap_mode(TextWrapMode::Wrap)
127    }
128
129    /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`].
130    #[inline]
131    pub fn truncate(self) -> Self {
132        self.wrap_mode(TextWrapMode::Truncate)
133    }
134
135    /// Override background fill color. Note that this will override any on-hover effects.
136    /// Calling this will also turn on the frame.
137    #[inline]
138    pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
139        self.fill = Some(fill.into());
140        self
141    }
142
143    /// Override button stroke. Note that this will override any on-hover effects.
144    /// Calling this will also turn on the frame.
145    #[inline]
146    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
147        self.stroke = Some(stroke.into());
148        self.frame = Some(true);
149        self
150    }
151
152    /// Make this a small button, suitable for embedding into text.
153    #[inline]
154    pub fn small(mut self) -> Self {
155        self.small = true;
156        self
157    }
158
159    /// Turn off the frame
160    #[inline]
161    pub fn frame(mut self, frame: bool) -> Self {
162        self.frame = Some(frame);
163        self
164    }
165
166    /// If `false`, the button will not have a frame when inactive.
167    ///
168    /// Default: `true`.
169    ///
170    /// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting
171    /// has no effect.
172    #[inline]
173    pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
174        self.frame_when_inactive = frame_when_inactive;
175        self
176    }
177
178    /// By default, buttons senses clicks.
179    /// Change this to a drag-button with `Sense::drag()`.
180    #[inline]
181    pub fn sense(mut self, sense: Sense) -> Self {
182        self.layout = self.layout.sense(sense);
183        self
184    }
185
186    /// Set the minimum size of the button.
187    #[inline]
188    pub fn min_size(mut self, min_size: Vec2) -> Self {
189        self.min_size = min_size;
190        self
191    }
192
193    /// Set the rounding of the button.
194    #[inline]
195    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
196        self.corner_radius = Some(corner_radius.into());
197        self
198    }
199
200    #[inline]
201    #[deprecated = "Renamed to `corner_radius`"]
202    pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
203        self.corner_radius(corner_radius)
204    }
205
206    /// If true, the tint of the image is multiplied by the widget text color.
207    ///
208    /// This makes sense for images that are white, that should have the same color as the text color.
209    /// This will also make the icon color depend on hover state.
210    ///
211    /// Default: `false`.
212    #[inline]
213    pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
214        self.image_tint_follows_text_color = image_tint_follows_text_color;
215        self
216    }
217
218    /// Show some text on the right side of the button, in weak color.
219    ///
220    /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
221    ///
222    /// The text can be created with [`crate::Context::format_shortcut`].
223    ///
224    /// See also [`Self::right_text`].
225    #[inline]
226    pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
227        let mut atom = shortcut_text.into();
228        atom.kind = match atom.kind {
229            AtomKind::Text(text) => AtomKind::Text(text.weak()),
230            other => other,
231        };
232        self.layout.push_right(Atom::grow());
233        self.layout.push_right(atom);
234        self
235    }
236
237    /// Show some text on the right side of the button.
238    #[inline]
239    pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
240        self.layout.push_right(Atom::grow());
241        self.layout.push_right(right_text.into());
242        self
243    }
244
245    /// If `true`, mark this button as "selected".
246    #[inline]
247    pub fn selected(mut self, selected: bool) -> Self {
248        self.selected = selected;
249        self
250    }
251
252    /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
253    pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
254        let Button {
255            mut layout,
256            fill,
257            stroke,
258            small,
259            frame,
260            frame_when_inactive,
261            mut min_size,
262            corner_radius,
263            selected,
264            image_tint_follows_text_color,
265            limit_image_size,
266        } = self;
267
268        if !small {
269            min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
270        }
271
272        if limit_image_size {
273            layout.map_atoms(|atom| {
274                if matches!(&atom.kind, AtomKind::Image(_)) {
275                    atom.atom_max_height_font_size(ui)
276                } else {
277                    atom
278                }
279            });
280        }
281
282        let text = layout.text().map(String::from);
283
284        let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
285
286        let mut button_padding = if has_frame_margin {
287            ui.spacing().button_padding
288        } else {
289            Vec2::ZERO
290        };
291        if small {
292            button_padding.y = 0.0;
293        }
294
295        let mut prepared = layout
296            .frame(Frame::new().inner_margin(button_padding))
297            .min_size(min_size)
298            .allocate(ui);
299
300        let response = if ui.is_rect_visible(prepared.response.rect) {
301            let visuals = ui.style().interact_selectable(&prepared.response, selected);
302
303            let visible_frame = if frame_when_inactive {
304                has_frame_margin
305            } else {
306                has_frame_margin
307                    && (prepared.response.hovered()
308                        || prepared.response.is_pointer_button_down_on()
309                        || prepared.response.has_focus())
310            };
311
312            if image_tint_follows_text_color {
313                prepared.map_images(|image| image.tint(visuals.text_color()));
314            }
315
316            prepared.fallback_text_color = visuals.text_color();
317
318            if visible_frame {
319                let stroke = stroke.unwrap_or(visuals.bg_stroke);
320                let fill = fill.unwrap_or(visuals.weak_bg_fill);
321                prepared.frame = prepared
322                    .frame
323                    .inner_margin(
324                        button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width),
325                    )
326                    .outer_margin(-Vec2::splat(visuals.expansion))
327                    .fill(fill)
328                    .stroke(stroke)
329                    .corner_radius(corner_radius.unwrap_or(visuals.corner_radius));
330            };
331
332            prepared.paint(ui)
333        } else {
334            AtomLayoutResponse::empty(prepared.response)
335        };
336
337        response.response.widget_info(|| {
338            if let Some(text) = &text {
339                WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
340            } else {
341                WidgetInfo::new(WidgetType::Button)
342            }
343        });
344
345        response
346    }
347}
348
349impl Widget for Button<'_> {
350    fn ui(self, ui: &mut Ui) -> Response {
351        self.atom_ui(ui).response
352    }
353}