Skip to main content

egui/widgets/
button.rs

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