Skip to main content

egui/widgets/
button.rs

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