1use crate::{
2 widgets, Align, Color32, CornerRadius, Image, NumExt, Rect, Response, Sense, Stroke, TextStyle,
3 TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
4};
5
6#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
25pub struct Button<'a> {
26 image: Option<Image<'a>>,
27 text: Option<WidgetText>,
28 shortcut_text: WidgetText,
29 wrap_mode: Option<TextWrapMode>,
30
31 fill: Option<Color32>,
33 stroke: Option<Stroke>,
34 sense: Sense,
35 small: bool,
36 frame: Option<bool>,
37 min_size: Vec2,
38 corner_radius: Option<CornerRadius>,
39 selected: bool,
40 image_tint_follows_text_color: bool,
41}
42
43impl<'a> Button<'a> {
44 pub fn new(text: impl Into<WidgetText>) -> Self {
45 Self::opt_image_and_text(None, Some(text.into()))
46 }
47
48 #[allow(clippy::needless_pass_by_value)]
50 pub fn image(image: impl Into<Image<'a>>) -> Self {
51 Self::opt_image_and_text(Some(image.into()), None)
52 }
53
54 #[allow(clippy::needless_pass_by_value)]
56 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
57 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
58 }
59
60 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
61 Self {
62 text,
63 image,
64 shortcut_text: Default::default(),
65 wrap_mode: None,
66 fill: None,
67 stroke: None,
68 sense: Sense::click(),
69 small: false,
70 frame: None,
71 min_size: Vec2::ZERO,
72 corner_radius: None,
73 selected: false,
74 image_tint_follows_text_color: false,
75 }
76 }
77
78 #[inline]
84 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
85 self.wrap_mode = Some(wrap_mode);
86 self
87 }
88
89 #[inline]
91 pub fn wrap(mut self) -> Self {
92 self.wrap_mode = Some(TextWrapMode::Wrap);
93
94 self
95 }
96
97 #[inline]
99 pub fn truncate(mut self) -> Self {
100 self.wrap_mode = Some(TextWrapMode::Truncate);
101 self
102 }
103
104 #[inline]
107 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
108 self.fill = Some(fill.into());
109 self.frame = Some(true);
110 self
111 }
112
113 #[inline]
116 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
117 self.stroke = Some(stroke.into());
118 self.frame = Some(true);
119 self
120 }
121
122 #[inline]
124 pub fn small(mut self) -> Self {
125 if let Some(text) = self.text {
126 self.text = Some(text.text_style(TextStyle::Body));
127 }
128 self.small = true;
129 self
130 }
131
132 #[inline]
134 pub fn frame(mut self, frame: bool) -> Self {
135 self.frame = Some(frame);
136 self
137 }
138
139 #[inline]
142 pub fn sense(mut self, sense: Sense) -> Self {
143 self.sense = sense;
144 self
145 }
146
147 #[inline]
149 pub fn min_size(mut self, min_size: Vec2) -> Self {
150 self.min_size = min_size;
151 self
152 }
153
154 #[inline]
156 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
157 self.corner_radius = Some(corner_radius.into());
158 self
159 }
160
161 #[inline]
162 #[deprecated = "Renamed to `corner_radius`"]
163 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
164 self.corner_radius(corner_radius)
165 }
166
167 #[inline]
174 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
175 self.image_tint_follows_text_color = image_tint_follows_text_color;
176 self
177 }
178
179 #[inline]
185 pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
186 self.shortcut_text = shortcut_text.into();
187 self
188 }
189
190 #[inline]
192 pub fn selected(mut self, selected: bool) -> Self {
193 self.selected = selected;
194 self
195 }
196}
197
198impl Widget for Button<'_> {
199 fn ui(self, ui: &mut Ui) -> Response {
200 let Button {
201 text,
202 image,
203 shortcut_text,
204 wrap_mode,
205 fill,
206 stroke,
207 sense,
208 small,
209 frame,
210 min_size,
211 corner_radius,
212 selected,
213 image_tint_follows_text_color,
214 } = self;
215
216 let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
217
218 let mut button_padding = if frame {
219 ui.spacing().button_padding
220 } else {
221 Vec2::ZERO
222 };
223 if small {
224 button_padding.y = 0.0;
225 }
226
227 let space_available_for_image = if let Some(text) = &text {
228 let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
229 Vec2::splat(font_height) } else {
231 ui.available_size() - 2.0 * button_padding
232 };
233
234 let image_size = if let Some(image) = &image {
235 image
236 .load_and_calc_size(ui, space_available_for_image)
237 .unwrap_or(space_available_for_image)
238 } else {
239 Vec2::ZERO
240 };
241
242 let gap_before_shortcut_text = ui.spacing().item_spacing.x;
243
244 let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
245 if image.is_some() {
246 text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
247 }
248
249 let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
251 shortcut_text.into_galley(
252 ui,
253 Some(TextWrapMode::Extend),
254 f32::INFINITY,
255 TextStyle::Button,
256 )
257 });
258
259 if let Some(shortcut_galley) = &shortcut_galley {
260 text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
262 }
263
264 let galley =
265 text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
266
267 let mut desired_size = Vec2::ZERO;
268 if image.is_some() {
269 desired_size.x += image_size.x;
270 desired_size.y = desired_size.y.max(image_size.y);
271 }
272 if image.is_some() && galley.is_some() {
273 desired_size.x += ui.spacing().icon_spacing;
274 }
275 if let Some(galley) = &galley {
276 desired_size.x += galley.size().x;
277 desired_size.y = desired_size.y.max(galley.size().y);
278 }
279 if let Some(shortcut_galley) = &shortcut_galley {
280 desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
281 desired_size.y = desired_size.y.max(shortcut_galley.size().y);
282 }
283 desired_size += 2.0 * button_padding;
284 if !small {
285 desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
286 }
287 desired_size = desired_size.at_least(min_size);
288
289 let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
290 response.widget_info(|| {
291 if let Some(galley) = &galley {
292 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
293 } else {
294 WidgetInfo::new(WidgetType::Button)
295 }
296 });
297
298 if ui.is_rect_visible(rect) {
299 let visuals = ui.style().interact(&response);
300
301 let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected {
302 let selection = ui.visuals().selection;
303 (
304 Vec2::ZERO,
305 CornerRadius::ZERO,
306 selection.bg_fill,
307 selection.stroke,
308 )
309 } else if frame {
310 let expansion = Vec2::splat(visuals.expansion);
311 (
312 expansion,
313 visuals.corner_radius,
314 visuals.weak_bg_fill,
315 visuals.bg_stroke,
316 )
317 } else {
318 Default::default()
319 };
320 let frame_cr = corner_radius.unwrap_or(frame_cr);
321 let frame_fill = fill.unwrap_or(frame_fill);
322 let frame_stroke = stroke.unwrap_or(frame_stroke);
323 ui.painter().rect(
324 rect.expand2(frame_expansion),
325 frame_cr,
326 frame_fill,
327 frame_stroke,
328 epaint::StrokeKind::Inside,
329 );
330
331 let mut cursor_x = rect.min.x + button_padding.x;
332
333 if let Some(image) = &image {
334 let mut image_pos = ui
335 .layout()
336 .align_size_within_rect(image_size, rect.shrink2(button_padding))
337 .min;
338 if galley.is_some() || shortcut_galley.is_some() {
339 image_pos.x = cursor_x;
340 }
341 let image_rect = Rect::from_min_size(image_pos, image_size);
342 cursor_x += image_size.x;
343 let tlr = image.load_for_size(ui.ctx(), image_size);
344 let mut image_options = image.image_options().clone();
345 if image_tint_follows_text_color {
346 image_options.tint = image_options.tint * visuals.text_color();
347 }
348 widgets::image::paint_texture_load_result(
349 ui,
350 &tlr,
351 image_rect,
352 image.show_loading_spinner,
353 &image_options,
354 None,
355 );
356 response = widgets::image::texture_load_result_response(
357 &image.source(ui.ctx()),
358 &tlr,
359 response,
360 );
361 }
362
363 if image.is_some() && galley.is_some() {
364 cursor_x += ui.spacing().icon_spacing;
365 }
366
367 if let Some(galley) = galley {
368 let mut text_pos = ui
369 .layout()
370 .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
371 .min;
372 if image.is_some() || shortcut_galley.is_some() {
373 text_pos.x = cursor_x;
374 }
375 ui.painter().galley(text_pos, galley, visuals.text_color());
376 }
377
378 if let Some(shortcut_galley) = shortcut_galley {
379 let layout = if ui.layout().is_horizontal() {
381 ui.layout().with_main_align(Align::Max)
382 } else {
383 ui.layout().with_cross_align(Align::Max)
384 };
385 let shortcut_text_pos = layout
386 .align_size_within_rect(shortcut_galley.size(), rect.shrink2(button_padding))
387 .min;
388 ui.painter().galley(
389 shortcut_text_pos,
390 shortcut_galley,
391 ui.visuals().weak_text_color(),
392 );
393 }
394 }
395
396 if let Some(cursor) = ui.visuals().interact_cursor {
397 if response.hovered() {
398 ui.ctx().set_cursor_icon(cursor);
399 }
400 }
401
402 response
403 }
404}