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