1use epaint::Margin;
2
3use crate::{
4 Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
5 Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2,
6 Widget, WidgetInfo, WidgetText, WidgetType,
7 widget_style::{ButtonStyle, 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: bool,
39 image_tint_follows_text_color: bool,
40 limit_image_size: bool,
41}
42
43impl<'a> Button<'a> {
44 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
45 Self {
46 layout: AtomLayout::new(atoms.into_atoms())
47 .sense(Sense::click())
48 .fallback_font(TextStyle::Button),
49 fill: None,
50 stroke: None,
51 small: false,
52 frame: None,
53 frame_when_inactive: true,
54 min_size: Vec2::ZERO,
55 corner_radius: None,
56 selected: false,
57 image_tint_follows_text_color: false,
58 limit_image_size: false,
59 }
60 }
61
62 pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
77 Self::new(atoms)
78 .selected(selected)
79 .frame_when_inactive(selected)
80 .frame(true)
81 }
82
83 pub fn image(image: impl Into<Image<'a>>) -> Self {
88 Self::opt_image_and_text(Some(image.into()), None)
89 }
90
91 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
96 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
97 }
98
99 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
104 let mut button = Self::new(());
105 if let Some(image) = image {
106 button.layout.push_right(image);
107 }
108 if let Some(text) = text {
109 button.layout.push_right(text);
110 }
111 button.limit_image_size = true;
112 button
113 }
114
115 #[inline]
121 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
122 self.layout = self.layout.wrap_mode(wrap_mode);
123 self
124 }
125
126 #[inline]
128 pub fn wrap(self) -> Self {
129 self.wrap_mode(TextWrapMode::Wrap)
130 }
131
132 #[inline]
134 pub fn truncate(self) -> Self {
135 self.wrap_mode(TextWrapMode::Truncate)
136 }
137
138 #[inline]
141 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
142 self.fill = Some(fill.into());
143 self
144 }
145
146 #[inline]
149 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
150 self.stroke = Some(stroke.into());
151 self.frame = Some(true);
152 self
153 }
154
155 #[inline]
157 pub fn small(mut self) -> Self {
158 self.small = true;
159 self
160 }
161
162 #[inline]
164 pub fn frame(mut self, frame: bool) -> Self {
165 self.frame = Some(frame);
166 self
167 }
168
169 #[inline]
176 pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
177 self.frame_when_inactive = frame_when_inactive;
178 self
179 }
180
181 #[inline]
184 pub fn sense(mut self, sense: Sense) -> Self {
185 self.layout = self.layout.sense(sense);
186 self
187 }
188
189 #[inline]
191 pub fn min_size(mut self, min_size: Vec2) -> Self {
192 self.min_size = min_size;
193 self
194 }
195
196 #[inline]
198 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
199 self.corner_radius = Some(corner_radius.into());
200 self
201 }
202
203 #[inline]
204 #[deprecated = "Renamed to `corner_radius`"]
205 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
206 self.corner_radius(corner_radius)
207 }
208
209 #[inline]
216 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
217 self.image_tint_follows_text_color = image_tint_follows_text_color;
218 self
219 }
220
221 #[inline]
229 pub fn shortcut_text(mut self, shortcut_text: impl IntoAtoms<'a>) -> Self {
230 self.layout.push_right(Atom::grow());
231
232 for mut atom in shortcut_text.into_atoms() {
233 atom.kind = match atom.kind {
234 AtomKind::Text(text) => AtomKind::Text(text.weak()),
235 other => other,
236 };
237 self.layout.push_right(atom);
238 }
239
240 self
241 }
242
243 #[inline]
245 pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
246 self.layout.push_left(Atom::grow());
247
248 for atom in left_text.into_atoms() {
249 self.layout.push_left(atom);
250 }
251
252 self
253 }
254
255 #[inline]
257 pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
258 self.layout.push_right(Atom::grow());
259
260 for atom in right_text.into_atoms() {
261 self.layout.push_right(atom);
262 }
263
264 self
265 }
266
267 #[inline]
269 pub fn selected(mut self, selected: bool) -> Self {
270 self.selected = selected;
271 self
272 }
273
274 #[inline]
276 pub fn gap(mut self, gap: f32) -> Self {
277 self.layout = self.layout.gap(gap);
278 self
279 }
280
281 pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
283 let Button {
284 mut layout,
285 fill,
286 stroke,
287 small,
288 frame,
289 frame_when_inactive,
290 mut min_size,
291 corner_radius,
292 selected,
293 image_tint_follows_text_color,
294 limit_image_size,
295 } = self;
296
297 if !small {
299 min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
300 }
301
302 if limit_image_size {
303 layout.map_atoms(|atom| {
304 if matches!(&atom.kind, AtomKind::Image(_)) {
305 atom.atom_max_height_font_size(ui)
306 } else {
307 atom
308 }
309 });
310 }
311
312 let text = layout.text().map(String::from);
313
314 let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
315
316 let id = ui.next_auto_id();
317 let response: Option<Response> = ui.ctx().read_response(id);
318 let state = response.map(|r| r.widget_state()).unwrap_or_default();
319
320 let ButtonStyle { frame, text_style } = ui.style().button_style(state, selected);
321
322 let mut button_padding = if has_frame_margin {
323 frame.inner_margin
324 } else {
325 Margin::ZERO
326 };
327
328 if small {
329 button_padding.bottom = 0;
330 button_padding.top = 0;
331 }
332
333 let mut frame = frame;
335 if let Some(fill) = fill {
336 frame = frame.fill(fill);
337 }
338 if let Some(corner_radius) = corner_radius {
339 frame = frame.corner_radius(corner_radius);
340 }
341 if let Some(stroke) = stroke {
342 frame = frame.stroke(stroke);
343 }
344
345 frame = frame.inner_margin(button_padding);
346
347 layout = layout
349 .fallback_font(text_style.font_id.clone())
350 .fallback_text_color(text_style.color);
351
352 layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
354 layout.frame(frame)
355 } else {
356 layout.frame(Frame::new().inner_margin(frame.inner_margin))
357 };
358
359 let mut prepared = layout.min_size(min_size).allocate(ui);
360
361 let response = if ui.is_rect_visible(prepared.response.rect) {
363 if image_tint_follows_text_color {
364 prepared.map_images(|image| image.tint(text_style.color));
365 }
366
367 prepared.fallback_text_color = text_style.color;
368
369 prepared.paint(ui)
370 } else {
371 AtomLayoutResponse::empty(prepared.response)
372 };
373
374 if let Some(cursor) = ui.visuals().interact_cursor
375 && response.response.hovered()
376 {
377 ui.ctx().set_cursor_icon(cursor);
378 }
379
380 response.response.widget_info(|| {
381 if let Some(text) = &text {
382 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
383 } else {
384 WidgetInfo::new(WidgetType::Button)
385 }
386 });
387
388 response
389 }
390}
391
392impl Widget for Button<'_> {
393 fn ui(self, ui: &mut Ui) -> Response {
394 self.atom_ui(ui).response
395 }
396}