1use 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 zeus_theme::visuals::ButtonVisuals;
8
9#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
10pub struct Button<'a> {
11 layout: AtomLayout<'a>,
12 visuals: Option<ButtonVisuals>,
13 bg_color: Option<Color32>,
14 small: bool,
15 frame_when_inactive: bool,
16 min_size: Vec2,
17 selected: bool,
18 image_tint_follows_text_color: bool,
19 limit_image_size: bool,
20}
21
22impl<'a> Button<'a> {
23 pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
24 Self {
25 layout: AtomLayout::new(atoms.into_atoms())
26 .sense(Sense::click())
27 .fallback_font(TextStyle::Button),
28 visuals: None,
29 bg_color: None,
30 small: false,
31 frame_when_inactive: true,
32 min_size: Vec2::ZERO,
33 selected: false,
34 image_tint_follows_text_color: false,
35 limit_image_size: false,
36 }
37 }
38
39 pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
54 Self::new(atoms).selected(selected).frame_when_inactive(selected)
55 }
56
57 pub fn image(image: impl Into<Image<'a>>) -> Self {
62 Self::opt_image_and_text(Some(image.into()), None)
63 }
64
65 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
70 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
71 }
72
73 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
78 let mut button = Self::new(());
79 if let Some(image) = image {
80 button.layout.push_right(image);
81 }
82 if let Some(text) = text {
83 button.layout.push_right(text);
84 }
85 button.limit_image_size = true;
86 button
87 }
88
89 #[inline]
95 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
96 self.layout = self.layout.wrap_mode(wrap_mode);
97 self
98 }
99
100 #[inline]
102 pub fn wrap(self) -> Self {
103 self.wrap_mode(TextWrapMode::Wrap)
104 }
105
106 #[inline]
108 pub fn truncate(self) -> Self {
109 self.wrap_mode(TextWrapMode::Truncate)
110 }
111
112 #[inline]
114 pub fn small(mut self) -> Self {
115 self.small = true;
116 self
117 }
118
119 #[inline]
126 pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
127 self.frame_when_inactive = frame_when_inactive;
128 self
129 }
130
131 #[inline]
134 pub fn sense(mut self, sense: Sense) -> Self {
135 self.layout = self.layout.sense(sense);
136 self
137 }
138
139 #[inline]
141 pub fn min_size(mut self, min_size: Vec2) -> Self {
142 self.min_size = min_size;
143 self
144 }
145
146 #[inline]
153 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
154 self.image_tint_follows_text_color = image_tint_follows_text_color;
155 self
156 }
157
158 #[inline]
166 pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
167 let mut atom = shortcut_text.into();
168 atom.kind = match atom.kind {
169 AtomKind::Text(text) => AtomKind::Text(text.weak()),
170 other => other,
171 };
172 self.layout.push_right(Atom::grow());
173 self.layout.push_right(atom);
174 self
175 }
176
177 #[inline]
179 pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
180 self.layout.push_right(Atom::grow());
181 self.layout.push_right(right_text.into());
182 self
183 }
184
185 #[inline]
187 pub fn selected(mut self, selected: bool) -> Self {
188 self.selected = selected;
189 self
190 }
191
192 #[inline]
194 pub fn visuals(mut self, visuals: ButtonVisuals) -> Self {
195 self.visuals = Some(visuals);
196 self
197 }
198
199 #[inline]
201 pub fn bg_color(mut self, color: Color32) -> Self {
202 self.bg_color = Some(color);
203 self
204 }
205
206 pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
208 let Button {
209 mut layout,
210 bg_color,
211 small,
212 visuals,
213 frame_when_inactive,
214 mut min_size,
215 selected,
216 image_tint_follows_text_color,
217 limit_image_size,
218 } = self;
219
220 if !small {
221 min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
222 }
223 if limit_image_size {
224 layout.map_atoms(|atom| {
225 if matches!(&atom.kind, AtomKind::Image(_)) {
226 atom.atom_max_height_font_size(ui)
227 } else {
228 atom
229 }
230 });
231 }
232
233 let text = layout.text().map(String::from);
234
235 let has_frame_margin = visuals.is_some() || ui.visuals().button_frame;
236 let mut button_padding = if has_frame_margin {
237 ui.spacing().button_padding
238 } else {
239 Vec2::ZERO
240 };
241
242 if small {
243 button_padding.y = 0.0;
244 }
245
246 let frame = Frame::new().inner_margin(button_padding);
247 let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
248
249 let response = if ui.is_rect_visible(prepared.response.rect) {
250 let interact_visuals = ui.style().interact_selectable(&prepared.response, selected);
251
252 let is_active = prepared.response.is_pointer_button_down_on(); let is_hovered = prepared.response.hovered();
254
255 let (fill, stroke, corner_radius) = if let Some(v) = &visuals {
257 if is_active {
258 (v.bg_click, v.border_click, v.corner_radius)
259 } else if is_hovered {
260 (v.bg_hover, v.border_hover, v.corner_radius)
261 } else {
262 let bg = bg_color.unwrap_or(v.bg);
263 (bg, v.border, v.corner_radius)
264 }
265 } else {
266 (
267 interact_visuals.weak_bg_fill,
268 interact_visuals.bg_stroke,
269 interact_visuals.corner_radius,
270 )
271 };
272
273 let fill = match selected {
274 false => fill,
275 true => {
276 visuals.as_ref().map(|v| v.bg_selected).unwrap_or(interact_visuals.weak_bg_fill)
277 }
278 };
279
280 let text_color = visuals.as_ref().map(|v| v.text).unwrap_or(interact_visuals.text_color());
281
282 let visible_frame = if frame_when_inactive {
283 has_frame_margin
284 } else {
285 has_frame_margin && (is_hovered || is_active || prepared.response.has_focus())
286 };
287
288 if image_tint_follows_text_color {
289 prepared.map_images(|image| image.tint(text_color));
290 }
291 prepared.fallback_text_color = text_color;
292
293 if visible_frame {
294 let shadow = visuals.as_ref().map(|v| v.shadow).unwrap_or(Shadow::NONE);
295 prepared.frame = prepared
296 .frame
297 .inner_margin(
298 button_padding + Vec2::splat(interact_visuals.expansion)
299 - Vec2::splat(stroke.width),
300 )
301 .outer_margin(-Vec2::splat(interact_visuals.expansion))
302 .fill(fill)
303 .stroke(stroke)
304 .corner_radius(corner_radius)
305 .shadow(shadow);
306 }
307
308 prepared.paint(ui)
309 } else {
310 AtomLayoutResponse::empty(prepared.response)
311 };
312
313 response.response.widget_info(|| {
314 if let Some(text) = &text {
315 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
316 } else {
317 WidgetInfo::new(WidgetType::Button)
318 }
319 });
320
321 response
322 }
323}
324
325impl Widget for Button<'_> {
326 fn ui(self, ui: &mut Ui) -> Response {
327 self.atom_ui(ui).response
328 }
329}