1use epaint::Margin;
2
3use crate::{
4 Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Atoms, Color32, CornerRadius,
5 Frame, Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui,
6 Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
7 widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState},
8};
9
10#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
29pub struct Button<'a> {
30 layout: AtomLayout<'a>,
31 fill: Option<Color32>,
32 stroke: Option<Stroke>,
33 small: bool,
34 frame: Option<bool>,
35 frame_when_inactive: bool,
36 min_size: Vec2,
37 corner_radius: Option<CornerRadius>,
38 selected: Option<bool>,
39 image_tint_follows_text_color: bool,
40 limit_image_size: bool,
41 classes: Classes,
42}
43
44impl<'a> Button<'a> {
45 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
46 Self {
47 layout: AtomLayout::new(atoms.into_atoms())
48 .sense(Sense::click())
49 .fallback_font(TextStyle::Button),
50 fill: None,
51 stroke: None,
52 small: false,
53 frame: None,
54 frame_when_inactive: true,
55 min_size: Vec2::ZERO,
56 corner_radius: None,
57 selected: None,
58 image_tint_follows_text_color: false,
59 limit_image_size: false,
60 classes: Classes::default(),
61 }
62 }
63
64 pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
79 Self::new(atoms)
80 .selected(selected)
81 .frame_when_inactive(selected)
82 .frame(true)
83 }
84
85 pub fn image(image: impl Into<Image<'a>>) -> Self {
90 Self::opt_image_and_text(Some(image.into()), None)
91 }
92
93 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
98 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
99 }
100
101 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
106 let mut button = Self::new(());
107 if let Some(image) = image {
108 button.layout.push_right(image);
109 }
110 if let Some(text) = text {
111 button.layout.push_right(text);
112 }
113 button.limit_image_size = true;
114 button
115 }
116
117 #[inline]
123 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
124 self.layout = self.layout.wrap_mode(wrap_mode);
125 self
126 }
127
128 #[inline]
130 pub fn wrap(self) -> Self {
131 self.wrap_mode(TextWrapMode::Wrap)
132 }
133
134 #[inline]
136 pub fn truncate(self) -> Self {
137 self.wrap_mode(TextWrapMode::Truncate)
138 }
139
140 #[inline]
143 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
144 self.fill = Some(fill.into());
145 self
146 }
147
148 #[inline]
151 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
152 self.stroke = Some(stroke.into());
153 self.frame = Some(true);
154 self
155 }
156
157 #[inline]
159 pub fn small(mut self) -> Self {
160 self.small = true;
161 self
162 }
163
164 #[inline]
166 pub fn frame(mut self, frame: bool) -> Self {
167 self.frame = Some(frame);
168 self
169 }
170
171 #[inline]
178 pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
179 self.frame_when_inactive = frame_when_inactive;
180 self
181 }
182
183 #[inline]
186 pub fn sense(mut self, sense: Sense) -> Self {
187 self.layout = self.layout.sense(sense);
188 self
189 }
190
191 #[inline]
193 pub fn min_size(mut self, min_size: Vec2) -> Self {
194 self.min_size = min_size;
195 self
196 }
197
198 #[inline]
200 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
201 self.corner_radius = Some(corner_radius.into());
202 self
203 }
204
205 #[inline]
212 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
213 self.image_tint_follows_text_color = image_tint_follows_text_color;
214 self
215 }
216
217 #[inline]
225 pub fn shortcut_text(mut self, shortcut_text: impl IntoAtoms<'a>) -> Self {
226 self.layout.push_right(Atom::grow());
227
228 for mut atom in shortcut_text.into_atoms() {
229 atom.kind = match atom.kind {
230 AtomKind::Text(text) => AtomKind::Text(text.weak()),
231 other => other,
232 };
233 self.layout.push_right(atom);
234 }
235
236 self
237 }
238
239 #[inline]
241 pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
242 self.layout.push_left(Atom::grow());
243
244 for atom in left_text.into_atoms() {
245 self.layout.push_left(atom);
246 }
247
248 self
249 }
250
251 #[inline]
253 pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
254 self.layout.push_right(Atom::grow());
255
256 for atom in right_text.into_atoms() {
257 self.layout.push_right(atom);
258 }
259
260 self
261 }
262
263 #[inline]
270 pub fn selected(mut self, selected: bool) -> Self {
271 self.selected = Some(selected);
272 self
273 }
274
275 #[inline]
277 pub fn gap(mut self, gap: f32) -> Self {
278 self.layout = self.layout.gap(gap);
279 self
280 }
281
282 pub fn atoms(&self) -> &Atoms<'a> {
286 &self.layout.atoms
287 }
288
289 pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
291 let Button {
292 mut layout,
293 fill,
294 stroke,
295 small,
296 frame,
297 frame_when_inactive,
298 mut min_size,
299 corner_radius,
300 selected,
301 image_tint_follows_text_color,
302 limit_image_size,
303 mut classes,
304 } = self;
305
306 if !small {
308 min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
309 }
310
311 if limit_image_size {
312 layout.map_atoms(|atom| {
313 if matches!(&atom.kind, AtomKind::Image(_)) {
314 atom.atom_max_height_font_size(ui)
315 } else {
316 atom
317 }
318 });
319 }
320
321 let text = layout.text().map(String::from);
322
323 let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
324
325 let id = ui.next_auto_id();
326 let response: Option<Response> = ui.ctx().read_response(id);
327 let state = response.map(|r| r.widget_state()).unwrap_or_default();
328
329 classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
330
331 let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
332
333 let mut button_padding = if has_frame_margin {
334 frame.inner_margin
335 } else {
336 Margin::ZERO
337 };
338
339 if small {
340 button_padding.bottom = 0;
341 button_padding.top = 0;
342 }
343
344 let mut frame = frame;
346 if let Some(fill) = fill {
347 frame = frame.fill(fill);
348 }
349 if let Some(corner_radius) = corner_radius {
350 frame = frame.corner_radius(corner_radius);
351 }
352 if let Some(stroke) = stroke {
353 frame = frame.stroke(stroke);
354 }
355
356 frame = frame.inner_margin(button_padding);
357
358 layout = layout
360 .fallback_font(text_style.font_id.clone())
361 .fallback_text_color(text_style.color);
362
363 layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
365 layout.frame(frame)
366 } else {
367 layout.frame(Frame::new().inner_margin(frame.inner_margin))
368 };
369
370 let mut prepared = layout.min_size(min_size).allocate(ui);
371
372 let response = if ui.is_rect_visible(prepared.response.rect) {
374 if image_tint_follows_text_color {
375 prepared.map_images(|image| image.tint(text_style.color));
376 }
377
378 prepared.fallback_text_color = text_style.color;
379
380 prepared.paint(ui)
381 } else {
382 AtomLayoutResponse::empty(prepared.response)
383 };
384
385 if let Some(cursor) = ui.visuals().interact_cursor
386 && response.response.hovered()
387 {
388 ui.ctx().set_cursor_icon(cursor);
389 }
390
391 response.response.widget_info(|| match (selected, &text) {
392 (Some(selected), Some(text)) => {
393 WidgetInfo::selected(WidgetType::Button, ui.is_enabled(), selected, text)
394 }
395 (Some(selected), None) => {
396 let mut info = WidgetInfo::new(WidgetType::Button);
397 info.enabled = ui.is_enabled();
398 info.selected = Some(selected);
399 info
400 }
401 (None, Some(text)) => WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text),
402 (None, None) => WidgetInfo::new(WidgetType::Button),
403 });
404
405 response
406 }
407}
408
409impl Widget for Button<'_> {
410 fn ui(self, ui: &mut Ui) -> Response {
411 self.atom_ui(ui).response
412 }
413}
414
415impl HasClasses for Button<'_> {
416 fn classes(&self) -> &Classes {
417 &self.classes
418 }
419
420 fn classes_mut(&mut self) -> &mut Classes {
421 &mut self.classes
422 }
423}