egui_elements/widgets/
button.rs1use egui::{
2 Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, Frame, Image, IntoAtoms,
3 NumExt as _, Response, Sense, Shadow, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo,
4 WidgetText, WidgetType,
5};
6
7use crate::theme::Theme;
8use crate::visuals::ButtonVisuals;
9
10#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
27pub struct Button<'a> {
28 layout: AtomLayout<'a>,
29 visuals: Option<ButtonVisuals>,
30 bg_color: Option<Color32>,
31 small: bool,
32 frame_when_inactive: bool,
33 min_size: Vec2,
34 selected: bool,
35 image_tint_follows_text_color: bool,
36 limit_image_size: bool,
37}
38
39impl<'a> Button<'a> {
40 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 visuals: None,
47 bg_color: None,
48 small: false,
49 frame_when_inactive: true,
50 min_size: Vec2::ZERO,
51 selected: false,
52 image_tint_follows_text_color: false,
53 limit_image_size: false,
54 }
55 }
56
57 pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
72 Self::new(atoms).selected(selected).frame_when_inactive(selected)
73 }
74
75 pub fn image(image: impl Into<Image<'a>>) -> Self {
80 Self::opt_image_and_text(Some(image.into()), None)
81 }
82
83 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
88 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
89 }
90
91 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
96 let mut button = Self::new(());
97 if let Some(image) = image {
98 button.layout.push_right(image);
99 }
100 if let Some(text) = text {
101 button.layout.push_right(text);
102 }
103 button.limit_image_size = true;
104 button
105 }
106
107 #[inline]
113 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
114 self.layout = self.layout.wrap_mode(wrap_mode);
115 self
116 }
117
118 #[inline]
120 pub fn wrap(self) -> Self {
121 self.wrap_mode(TextWrapMode::Wrap)
122 }
123
124 #[inline]
126 pub fn truncate(self) -> Self {
127 self.wrap_mode(TextWrapMode::Truncate)
128 }
129
130 #[inline]
132 pub fn small(mut self) -> Self {
133 self.small = true;
134 self
135 }
136
137 #[inline]
144 pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
145 self.frame_when_inactive = frame_when_inactive;
146 self
147 }
148
149 #[inline]
152 pub fn sense(mut self, sense: Sense) -> Self {
153 self.layout = self.layout.sense(sense);
154 self
155 }
156
157 #[inline]
159 pub fn min_size(mut self, min_size: Vec2) -> Self {
160 self.min_size = min_size;
161 self
162 }
163
164 #[inline]
171 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
172 self.image_tint_follows_text_color = image_tint_follows_text_color;
173 self
174 }
175
176 #[inline]
184 pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
185 let mut atom = shortcut_text.into();
186 atom.kind = match atom.kind {
187 AtomKind::Text(text) => AtomKind::Text(text.weak()),
188 other => other,
189 };
190 self.layout.push_right(Atom::grow());
191 self.layout.push_right(atom);
192 self
193 }
194
195 #[inline]
197 pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
198 self.layout.push_right(Atom::grow());
199 self.layout.push_right(right_text.into());
200 self
201 }
202
203 #[inline]
205 pub fn selected(mut self, selected: bool) -> Self {
206 self.selected = selected;
207 self
208 }
209
210 #[inline]
212 pub fn visuals(mut self, visuals: ButtonVisuals) -> Self {
213 self.visuals = Some(visuals);
214 self
215 }
216
217 #[inline]
219 pub fn bg_color(mut self, color: Color32) -> Self {
220 self.bg_color = Some(color);
221 self
222 }
223
224 pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
226 let Button {
227 mut layout,
228 bg_color,
229 small,
230 visuals,
231 frame_when_inactive,
232 mut min_size,
233 selected,
234 image_tint_follows_text_color,
235 limit_image_size,
236 } = self;
237
238 let visuals = visuals.or_else(|| Theme::button_visuals_from_ctx(ui.ctx()));
239
240 if !small {
241 min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
242 }
243 if limit_image_size {
244 layout.map_atoms(|atom| {
245 if matches!(&atom.kind, AtomKind::Image(_)) {
246 atom.atom_max_height_font_size(ui)
247 } else {
248 atom
249 }
250 });
251 }
252
253 let text = layout.text().map(String::from);
254
255 let has_frame_margin = visuals.is_some() || ui.visuals().button_frame;
256 let mut button_padding = if has_frame_margin {
257 ui.spacing().button_padding
258 } else {
259 Vec2::ZERO
260 };
261
262 if small {
263 button_padding.y = 0.0;
264 }
265
266 let frame = Frame::new().inner_margin(button_padding);
267 let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
268
269 let response = if ui.is_rect_visible(prepared.response.rect) {
270 let interact_visuals = ui.style().interact_selectable(&prepared.response, selected);
271
272 let is_active = prepared.response.is_pointer_button_down_on(); let is_hovered = prepared.response.hovered();
274
275 let (fill, stroke, corner_radius) = if let Some(v) = &visuals {
277 if is_active {
278 (v.bg_click, v.border_click, v.corner_radius)
279 } else if is_hovered {
280 (v.bg_hover, v.border_hover, v.corner_radius)
281 } else {
282 let bg = bg_color.unwrap_or(v.bg);
283 (bg, v.border, v.corner_radius)
284 }
285 } else {
286 (
287 interact_visuals.weak_bg_fill,
288 interact_visuals.bg_stroke,
289 interact_visuals.corner_radius,
290 )
291 };
292
293 let fill = match selected {
294 false => fill,
295 true => {
296 visuals.as_ref().map(|v| v.bg_selected).unwrap_or(interact_visuals.weak_bg_fill)
297 }
298 };
299
300 let text_color = visuals.as_ref().map(|v| v.text).unwrap_or(interact_visuals.text_color());
301
302 let visible_frame = if frame_when_inactive {
303 has_frame_margin
304 } else {
305 has_frame_margin && (is_hovered || is_active || prepared.response.has_focus())
306 };
307
308 if image_tint_follows_text_color {
309 prepared.map_images(|image| image.tint(text_color));
310 }
311 prepared.fallback_text_color = text_color;
312
313 if visible_frame {
314 let shadow = visuals.as_ref().map(|v| v.shadow).unwrap_or(Shadow::NONE);
315 prepared.frame = prepared
316 .frame
317 .inner_margin(
318 button_padding + Vec2::splat(interact_visuals.expansion)
319 - Vec2::splat(stroke.width),
320 )
321 .outer_margin(-Vec2::splat(interact_visuals.expansion))
322 .fill(fill)
323 .stroke(stroke)
324 .corner_radius(corner_radius)
325 .shadow(shadow);
326 }
327
328 prepared.paint(ui)
329 } else {
330 AtomLayoutResponse::empty(prepared.response)
331 };
332
333 response.response.widget_info(|| {
334 if let Some(text) = &text {
335 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
336 } else {
337 WidgetInfo::new(WidgetType::Button)
338 }
339 });
340
341 response
342 }
343}
344
345impl Widget for Button<'_> {
346 fn ui(self, ui: &mut Ui) -> Response {
347 self.atom_ui(ui).response
348 }
349}