1use crate::*;
2
3#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
22pub struct Button<'a> {
23 image: Option<Image<'a>>,
24 text: Option<WidgetText>,
25 shortcut_text: WidgetText,
26 wrap_mode: Option<TextWrapMode>,
27
28 fill: Option<Color32>,
30 stroke: Option<Stroke>,
31 sense: Sense,
32 small: bool,
33 frame: Option<bool>,
34 min_size: Vec2,
35 rounding: Option<Rounding>,
36 selected: bool,
37}
38
39impl<'a> Button<'a> {
40 pub fn new(text: impl Into<WidgetText>) -> Self {
41 Self::opt_image_and_text(None, Some(text.into()))
42 }
43
44 #[allow(clippy::needless_pass_by_value)]
46 pub fn image(image: impl Into<Image<'a>>) -> Self {
47 Self::opt_image_and_text(Some(image.into()), None)
48 }
49
50 #[allow(clippy::needless_pass_by_value)]
52 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
53 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
54 }
55
56 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
57 Self {
58 text,
59 image,
60 shortcut_text: Default::default(),
61 wrap_mode: None,
62 fill: None,
63 stroke: None,
64 sense: Sense::click(),
65 small: false,
66 frame: None,
67 min_size: Vec2::ZERO,
68 rounding: None,
69 selected: false,
70 }
71 }
72
73 #[inline]
79 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
80 self.wrap_mode = Some(wrap_mode);
81 self
82 }
83
84 #[inline]
86 pub fn wrap(mut self) -> Self {
87 self.wrap_mode = Some(TextWrapMode::Wrap);
88
89 self
90 }
91
92 #[inline]
94 pub fn truncate(mut self) -> Self {
95 self.wrap_mode = Some(TextWrapMode::Truncate);
96 self
97 }
98
99 #[inline]
102 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
103 self.fill = Some(fill.into());
104 self.frame = Some(true);
105 self
106 }
107
108 #[inline]
111 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
112 self.stroke = Some(stroke.into());
113 self.frame = Some(true);
114 self
115 }
116
117 #[inline]
119 pub fn small(mut self) -> Self {
120 if let Some(text) = self.text {
121 self.text = Some(text.text_style(TextStyle::Body));
122 }
123 self.small = true;
124 self
125 }
126
127 #[inline]
129 pub fn frame(mut self, frame: bool) -> Self {
130 self.frame = Some(frame);
131 self
132 }
133
134 #[inline]
137 pub fn sense(mut self, sense: Sense) -> Self {
138 self.sense = sense;
139 self
140 }
141
142 #[inline]
144 pub fn min_size(mut self, min_size: Vec2) -> Self {
145 self.min_size = min_size;
146 self
147 }
148
149 #[inline]
151 pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
152 self.rounding = Some(rounding.into());
153 self
154 }
155
156 #[inline]
162 pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
163 self.shortcut_text = shortcut_text.into();
164 self
165 }
166
167 #[inline]
169 pub fn selected(mut self, selected: bool) -> Self {
170 self.selected = selected;
171 self
172 }
173}
174
175impl Widget for Button<'_> {
176 fn ui(self, ui: &mut Ui) -> Response {
177 let Button {
178 text,
179 image,
180 shortcut_text,
181 wrap_mode,
182 fill,
183 stroke,
184 sense,
185 small,
186 frame,
187 min_size,
188 rounding,
189 selected,
190 } = self;
191
192 let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
193
194 let mut button_padding = if frame {
195 ui.spacing().button_padding
196 } else {
197 Vec2::ZERO
198 };
199 if small {
200 button_padding.y = 0.0;
201 }
202
203 let space_available_for_image = if let Some(text) = &text {
204 let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
205 Vec2::splat(font_height) } else {
207 ui.available_size() - 2.0 * button_padding
208 };
209
210 let image_size = if let Some(image) = &image {
211 image
212 .load_and_calc_size(ui, space_available_for_image)
213 .unwrap_or(space_available_for_image)
214 } else {
215 Vec2::ZERO
216 };
217
218 let gap_before_shortcut_text = ui.spacing().item_spacing.x;
219
220 let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
221 if image.is_some() {
222 text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
223 }
224
225 let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
227 shortcut_text.into_galley(
228 ui,
229 Some(TextWrapMode::Extend),
230 f32::INFINITY,
231 TextStyle::Button,
232 )
233 });
234
235 if let Some(shortcut_galley) = &shortcut_galley {
236 text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
238 }
239
240 let galley =
241 text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
242
243 let mut desired_size = Vec2::ZERO;
244 if image.is_some() {
245 desired_size.x += image_size.x;
246 desired_size.y = desired_size.y.max(image_size.y);
247 }
248 if image.is_some() && galley.is_some() {
249 desired_size.x += ui.spacing().icon_spacing;
250 }
251 if let Some(text) = &galley {
252 desired_size.x += text.size().x;
253 desired_size.y = desired_size.y.max(text.size().y);
254 }
255 if let Some(shortcut_galley) = &shortcut_galley {
256 desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
257 desired_size.y = desired_size.y.max(shortcut_galley.size().y);
258 }
259 desired_size += 2.0 * button_padding;
260 if !small {
261 desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
262 }
263 desired_size = desired_size.at_least(min_size);
264
265 let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
266 response.widget_info(|| {
267 if let Some(galley) = &galley {
268 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
269 } else {
270 WidgetInfo::new(WidgetType::Button)
271 }
272 });
273
274 if ui.is_rect_visible(rect) {
275 let visuals = ui.style().interact(&response);
276
277 let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
278 let selection = ui.visuals().selection;
279 (
280 Vec2::ZERO,
281 Rounding::ZERO,
282 selection.bg_fill,
283 selection.stroke,
284 )
285 } else if frame {
286 let expansion = Vec2::splat(visuals.expansion);
287 (
288 expansion,
289 visuals.rounding,
290 visuals.weak_bg_fill,
291 visuals.bg_stroke,
292 )
293 } else {
294 Default::default()
295 };
296 let frame_rounding = rounding.unwrap_or(frame_rounding);
297 let frame_fill = fill.unwrap_or(frame_fill);
298 let frame_stroke = stroke.unwrap_or(frame_stroke);
299 ui.painter().rect(
300 rect.expand2(frame_expansion),
301 frame_rounding,
302 frame_fill,
303 frame_stroke,
304 );
305
306 let mut cursor_x = rect.min.x + button_padding.x;
307
308 if let Some(image) = &image {
309 let image_rect = Rect::from_min_size(
310 pos2(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)),
311 image_size,
312 );
313 cursor_x += image_size.x;
314 let tlr = image.load_for_size(ui.ctx(), image_size);
315 widgets::image::paint_texture_load_result(
316 ui,
317 &tlr,
318 image_rect,
319 image.show_loading_spinner,
320 image.image_options(),
321 );
322 response = widgets::image::texture_load_result_response(
323 &image.source(ui.ctx()),
324 &tlr,
325 response,
326 );
327 }
328
329 if image.is_some() && galley.is_some() {
330 cursor_x += ui.spacing().icon_spacing;
331 }
332
333 if let Some(galley) = galley {
334 let text_pos = if image.is_some() || shortcut_galley.is_some() {
335 pos2(cursor_x, rect.center().y - 0.5 * galley.size().y)
336 } else {
337 ui.layout()
339 .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
340 .min
341 };
342 ui.painter().galley(text_pos, galley, visuals.text_color());
343 }
344
345 if let Some(shortcut_galley) = shortcut_galley {
346 let shortcut_text_pos = pos2(
347 rect.max.x - button_padding.x - shortcut_galley.size().x,
348 rect.center().y - 0.5 * shortcut_galley.size().y,
349 );
350 ui.painter().galley(
351 shortcut_text_pos,
352 shortcut_galley,
353 ui.visuals().weak_text_color(),
354 );
355 }
356 }
357
358 if let Some(cursor) = ui.visuals().interact_cursor {
359 if response.hovered {
360 ui.ctx().set_cursor_icon(cursor);
361 }
362 }
363
364 response
365 }
366}