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#[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 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 pub fn image(image: impl Into<Image<'a>>) -> Self {
83 Self::opt_image_and_text(Some(image.into()), None)
84 }
85
86 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 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 #[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 #[inline]
123 pub fn wrap(self) -> Self {
124 self.wrap_mode(TextWrapMode::Wrap)
125 }
126
127 #[inline]
129 pub fn truncate(self) -> Self {
130 self.wrap_mode(TextWrapMode::Truncate)
131 }
132
133 #[inline]
136 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
137 self.fill = Some(fill.into());
138 self
139 }
140
141 #[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 #[inline]
152 pub fn small(mut self) -> Self {
153 self.small = true;
154 self
155 }
156
157 #[inline]
159 pub fn frame(mut self, frame: bool) -> Self {
160 self.frame = Some(frame);
161 self
162 }
163
164 #[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 #[inline]
179 pub fn sense(mut self, sense: Sense) -> Self {
180 self.layout = self.layout.sense(sense);
181 self
182 }
183
184 #[inline]
186 pub fn min_size(mut self, min_size: Vec2) -> Self {
187 self.min_size = min_size;
188 self
189 }
190
191 #[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 #[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 #[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 #[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 #[inline]
245 pub fn selected(mut self, selected: bool) -> Self {
246 self.selected = selected;
247 self
248 }
249
250 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}