egui/widgets/
button.rs

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