1use egui::{
9 vec2, Color32, CornerRadius, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText,
10 WidgetType,
11};
12
13use crate::theme::{mix, Accent, Theme};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum ButtonSize {
18 Small,
20 Medium,
22 Large,
24}
25
26#[must_use = "Call `ui.add(...)` to render the button."]
37pub struct Button {
38 text: WidgetText,
39 accent: Accent,
40 size: ButtonSize,
41 outline: bool,
42 min_width: Option<f32>,
43 full_width: bool,
44 enabled: bool,
45}
46
47impl std::fmt::Debug for Button {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("Button")
50 .field("accent", &self.accent)
51 .field("size", &self.size)
52 .field("outline", &self.outline)
53 .field("min_width", &self.min_width)
54 .field("full_width", &self.full_width)
55 .field("enabled", &self.enabled)
56 .finish()
57 }
58}
59
60impl Button {
61 pub fn new(text: impl Into<WidgetText>) -> Self {
63 Self {
64 text: text.into(),
65 accent: Accent::Blue,
66 size: ButtonSize::Medium,
67 outline: false,
68 min_width: None,
69 full_width: false,
70 enabled: true,
71 }
72 }
73
74 #[inline]
77 pub fn accent(mut self, accent: Accent) -> Self {
78 self.accent = accent;
79 self
80 }
81
82 #[inline]
85 pub fn outline(mut self) -> Self {
86 self.outline = true;
87 self
88 }
89
90 #[inline]
92 pub fn size(mut self, size: ButtonSize) -> Self {
93 self.size = size;
94 self
95 }
96
97 #[inline]
99 pub fn min_width(mut self, w: f32) -> Self {
100 self.min_width = Some(w);
101 self
102 }
103
104 #[inline]
106 pub fn full_width(mut self) -> Self {
107 self.full_width = true;
108 self
109 }
110
111 #[inline]
113 pub fn enabled(mut self, enabled: bool) -> Self {
114 self.enabled = enabled;
115 self
116 }
117
118 fn padding(&self, theme: &Theme) -> Vec2 {
119 match self.size {
120 ButtonSize::Small => vec2(theme.control_padding_x * 0.6, theme.control_padding_y * 0.6),
121 ButtonSize::Medium => vec2(theme.control_padding_x, theme.control_padding_y),
122 ButtonSize::Large => vec2(
123 theme.control_padding_x * 1.25,
124 theme.control_padding_y * 1.2,
125 ),
126 }
127 }
128
129 fn font_size(&self, theme: &Theme) -> f32 {
130 match self.size {
131 ButtonSize::Small => theme.typography.small,
132 ButtonSize::Medium => theme.typography.button,
133 ButtonSize::Large => theme.typography.body + 1.0,
134 }
135 }
136}
137
138impl Widget for Button {
139 fn ui(self, ui: &mut Ui) -> Response {
140 let theme = Theme::current(ui.ctx());
141 let padding = self.padding(&theme);
142 let font_size = self.font_size(&theme);
143
144 let wrap_width = (ui.available_width() - 2.0 * padding.x).max(0.0);
145 let galley =
146 crate::theme::placeholder_galley(ui, self.text.text(), font_size, false, wrap_width);
147
148 let mut desired = galley.size() + 2.0 * padding;
149 desired.y = desired.y.max(font_size + 2.0 * padding.y);
150 if let Some(min_w) = self.min_width {
151 desired.x = desired.x.max(min_w);
152 }
153 if self.full_width {
154 desired.x = ui.available_width().max(desired.x);
155 }
156
157 let sense = if self.enabled {
158 Sense::click()
159 } else {
160 Sense::hover()
161 };
162 let (rect, response) = ui.allocate_exact_size(desired, sense);
163
164 let visible = ui.is_rect_visible(rect);
165 if visible {
166 let (fill, stroke, text_color) =
168 resolve_colors(&theme, self.accent, self.outline, self.enabled, &response);
169
170 let radius = CornerRadius::same(theme.control_radius as u8);
171 ui.painter()
172 .rect(rect, radius, fill, stroke, egui::StrokeKind::Inside);
173
174 let text_pos = rect.center();
175 ui.painter()
176 .galley(galley_top_left(rect, galley.size()), galley, text_color);
177 let _ = text_pos;
178 }
179
180 response.widget_info(|| {
181 WidgetInfo::labeled(WidgetType::Button, self.enabled, self.text.text())
182 });
183 response
184 }
185}
186
187fn galley_top_left(rect: egui::Rect, galley_size: Vec2) -> egui::Pos2 {
188 let center = rect.center();
189 center - galley_size * 0.5
190}
191
192fn resolve_colors(
193 theme: &Theme,
194 accent: Accent,
195 outline: bool,
196 enabled: bool,
197 response: &Response,
198) -> (Color32, Stroke, Color32) {
199 let p = &theme.palette;
200 if !enabled {
201 if outline {
202 return (
203 Color32::TRANSPARENT,
204 Stroke::new(1.0, p.border),
205 mix(p.text_muted, p.card, 0.4),
206 );
207 }
208 return (
209 mix(p.accent_fill(accent), p.card, 0.55),
210 Stroke::NONE,
211 mix(p.text, p.card, 0.4),
212 );
213 }
214 let is_down = response.is_pointer_button_down_on();
215 let is_hovered = response.hovered();
216
217 if outline {
218 let text = if is_hovered { p.text } else { p.text_muted };
219 let stroke_color = if is_hovered { p.text_muted } else { p.border };
220 let fill = if is_down {
221 with_alpha(p.text_muted, 30)
222 } else if is_hovered {
223 with_alpha(p.text_muted, 20)
224 } else {
225 Color32::TRANSPARENT
226 };
227 return (fill, Stroke::new(1.0, stroke_color), text);
228 }
229
230 let resting = p.accent_fill(accent);
231 let hover = p.accent_hover(accent);
232 let fill = if is_down {
233 mix(hover, Color32::BLACK, 0.08)
235 } else if is_hovered {
236 hover
237 } else {
238 resting
239 };
240 let stroke = if response.has_focus() {
241 Stroke::new(2.0, with_alpha(p.sky, 180))
242 } else {
243 Stroke::NONE
244 };
245 (fill, stroke, Color32::WHITE)
246}
247
248fn with_alpha(c: Color32, alpha: u8) -> Color32 {
249 crate::theme::with_alpha(c, alpha)
250}