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}