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#[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    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    pub fn image(image: impl Into<Image<'a>>) -> Self {
85        Self::opt_image_and_text(Some(image.into()), None)
86    }
87
88    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    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    #[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    #[inline]
125    pub fn wrap(self) -> Self {
126        self.wrap_mode(TextWrapMode::Wrap)
127    }
128
129    #[inline]
131    pub fn truncate(self) -> Self {
132        self.wrap_mode(TextWrapMode::Truncate)
133    }
134
135    #[inline]
138    pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
139        self.fill = Some(fill.into());
140        self
141    }
142
143    #[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    #[inline]
154    pub fn small(mut self) -> Self {
155        self.small = true;
156        self
157    }
158
159    #[inline]
161    pub fn frame(mut self, frame: bool) -> Self {
162        self.frame = Some(frame);
163        self
164    }
165
166    #[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    #[inline]
181    pub fn sense(mut self, sense: Sense) -> Self {
182        self.layout = self.layout.sense(sense);
183        self
184    }
185
186    #[inline]
188    pub fn min_size(mut self, min_size: Vec2) -> Self {
189        self.min_size = min_size;
190        self
191    }
192
193    #[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    #[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    #[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    #[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    #[inline]
247    pub fn selected(mut self, selected: bool) -> Self {
248        self.selected = selected;
249        self
250    }
251
252    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}