1use super::content::ContentContext;
11use crate::ext::ArmasContextExt;
12use crate::Theme;
13use egui::{Color32, Response, Sense, Ui, Vec2};
14
15const CORNER_RADIUS: f32 = 6.0; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum ButtonVariant {
21 #[default]
23 Default,
24 Secondary,
26 Outline,
28 Ghost,
30 Link,
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub enum ButtonSize {
37 Xs,
39 Small,
41 #[default]
43 Default,
44 Large,
46}
47
48impl ButtonSize {
49 const fn height(self) -> f32 {
50 match self {
51 Self::Xs => 22.0,
52 Self::Small => 32.0,
53 Self::Default => 36.0,
54 Self::Large => 40.0,
55 }
56 }
57
58 const fn padding_x(self) -> f32 {
59 match self {
60 Self::Xs => 6.0,
61 Self::Small => 12.0, Self::Default => 16.0, Self::Large => 24.0, }
65 }
66
67 const fn font_size(self, typo: &crate::theme::Typography) -> f32 {
68 match self {
69 Self::Xs => typo.xs,
70 _ => typo.base,
71 }
72 }
73}
74
75fn get_colors(
79 theme: &Theme,
80 variant: ButtonVariant,
81 enabled: bool,
82 hovered: bool,
83) -> (Color32, Color32, Color32) {
84 if enabled {
85 match variant {
86 ButtonVariant::Default => {
87 let bg = if hovered {
88 theme.primary().gamma_multiply(0.9)
89 } else {
90 theme.primary()
91 };
92 (bg, theme.primary_foreground(), Color32::TRANSPARENT)
93 }
94 ButtonVariant::Secondary => {
95 let bg = if hovered {
96 theme.secondary().gamma_multiply(0.8)
97 } else {
98 theme.secondary()
99 };
100 (bg, theme.secondary_foreground(), Color32::TRANSPARENT)
101 }
102 ButtonVariant::Outline => {
103 let bg = if hovered {
104 theme.accent()
105 } else {
106 Color32::TRANSPARENT
107 };
108 let text = if hovered {
109 theme.accent_foreground()
110 } else {
111 theme.foreground()
112 };
113 (bg, text, theme.border())
114 }
115 ButtonVariant::Ghost => {
116 let bg = if hovered {
117 theme.accent()
118 } else {
119 Color32::TRANSPARENT
120 };
121 let text = if hovered {
122 theme.accent_foreground()
123 } else {
124 theme.foreground()
125 };
126 (bg, text, Color32::TRANSPARENT)
127 }
128 ButtonVariant::Link => (Color32::TRANSPARENT, theme.primary(), Color32::TRANSPARENT),
129 }
130 } else {
131 (
132 theme.primary().gamma_multiply(0.5),
133 theme.primary_foreground().gamma_multiply(0.5),
134 Color32::TRANSPARENT,
135 )
136 }
137}
138
139fn draw_button_frame(
141 ui: &mut Ui,
142 rect: egui::Rect,
143 theme: &Theme,
144 variant: ButtonVariant,
145 enabled: bool,
146 hovered: bool,
147) -> (Color32, Color32) {
148 let (bg_color, text_color, border_color) = get_colors(theme, variant, enabled, hovered);
149
150 if bg_color != Color32::TRANSPARENT {
151 ui.painter().rect_filled(rect, CORNER_RADIUS, bg_color);
152 }
153 if border_color != Color32::TRANSPARENT {
154 ui.painter().rect_stroke(
155 rect,
156 CORNER_RADIUS,
157 egui::Stroke::new(1.0, border_color),
158 egui::StrokeKind::Inside,
159 );
160 }
161
162 (bg_color, text_color)
163}
164
165pub struct Button {
185 text: String,
186 variant: ButtonVariant,
187 size: ButtonSize,
188 enabled: bool,
189 full_width: bool,
190 min_width: Option<f32>,
191 custom_height: Option<f32>,
192 content_width: Option<f32>,
193}
194
195impl Button {
196 pub fn new(text: impl Into<String>) -> Self {
198 Self {
199 text: text.into(),
200 variant: ButtonVariant::Default,
201 size: ButtonSize::Default,
202 enabled: true,
203 full_width: false,
204 min_width: None,
205 custom_height: None,
206 content_width: None,
207 }
208 }
209
210 #[must_use]
212 pub const fn variant(mut self, variant: ButtonVariant) -> Self {
213 self.variant = variant;
214 self
215 }
216
217 #[must_use]
219 pub const fn size(mut self, size: ButtonSize) -> Self {
220 self.size = size;
221 self
222 }
223
224 #[must_use]
226 pub const fn enabled(mut self, enabled: bool) -> Self {
227 self.enabled = enabled;
228 self
229 }
230
231 #[must_use]
233 pub const fn full_width(mut self, full: bool) -> Self {
234 self.full_width = full;
235 self
236 }
237
238 #[must_use]
240 pub const fn min_width(mut self, width: f32) -> Self {
241 self.min_width = Some(width);
242 self
243 }
244
245 #[must_use]
247 pub const fn height(mut self, height: f32) -> Self {
248 self.custom_height = Some(height);
249 self
250 }
251
252 #[must_use]
257 pub const fn content_width(mut self, width: f32) -> Self {
258 self.content_width = Some(width);
259 self
260 }
261
262 pub fn show(self, ui: &mut Ui) -> Response {
264 let theme = ui.ctx().armas_theme();
265 let sense = if self.enabled {
266 Sense::click()
267 } else {
268 Sense::hover()
269 };
270
271 let height = self.custom_height.unwrap_or_else(|| self.size.height());
272 let padding_x = self.size.padding_x();
273
274 let font_id = egui::FontId::proportional(self.size.font_size(&theme.typography));
276 let text_galley =
277 ui.painter()
278 .layout_no_wrap(self.text.clone(), font_id, Color32::PLACEHOLDER);
279 let galley_size = text_galley.rect.size();
280 let text_width = galley_size.x;
281
282 let total_content_width = text_width + padding_x * 2.0;
283 let button_width = if self.full_width {
284 ui.available_width()
285 } else if let Some(min_w) = self.min_width {
286 total_content_width.max(min_w)
287 } else {
288 total_content_width
289 };
290
291 let button_size = Vec2::new(button_width, height);
292 let (rect, mut response) = ui.allocate_exact_size(button_size, sense);
293
294 if self.enabled && response.hovered() {
295 response = response.on_hover_cursor(egui::CursorIcon::PointingHand);
296 }
297
298 if ui.is_rect_visible(rect) {
299 let hovered = response.hovered() && self.enabled;
300 let (_, text_color) =
301 draw_button_frame(ui, rect, &theme, self.variant, self.enabled, hovered);
302
303 let text_pos = rect.center() - galley_size / 2.0;
305 ui.painter()
306 .galley(egui::pos2(text_pos.x, text_pos.y), text_galley, text_color);
307
308 if self.variant == ButtonVariant::Link && hovered {
310 let underline_y = text_pos.y + galley_size.y + 1.0;
311 ui.painter().line_segment(
312 [
313 egui::pos2(text_pos.x, underline_y),
314 egui::pos2(text_pos.x + galley_size.x, underline_y),
315 ],
316 egui::Stroke::new(1.0, text_color),
317 );
318 }
319 }
320
321 response
322 }
323
324 pub fn show_ui(self, ui: &mut Ui, content: impl FnOnce(&mut Ui, &ContentContext)) -> Response {
356 let theme = ui.ctx().armas_theme();
357 let sense = if self.enabled {
358 Sense::click()
359 } else {
360 Sense::hover()
361 };
362
363 let height = self.custom_height.unwrap_or_else(|| self.size.height());
364 let padding_x = self.size.padding_x();
365
366 let inner_width = self.content_width.unwrap_or(height - padding_x * 2.0);
368 let button_width = if self.full_width {
369 ui.available_width()
370 } else if let Some(min_w) = self.min_width {
371 (inner_width + padding_x * 2.0).max(min_w)
372 } else {
373 inner_width + padding_x * 2.0
374 };
375
376 let button_size = Vec2::new(button_width, height);
377 let (rect, mut response) = ui.allocate_exact_size(button_size, sense);
378
379 if self.enabled && response.hovered() {
380 response = response.on_hover_cursor(egui::CursorIcon::PointingHand);
381 }
382
383 if ui.is_rect_visible(rect) {
384 let hovered = response.hovered() && self.enabled;
385 let (_, text_color) =
386 draw_button_frame(ui, rect, &theme, self.variant, self.enabled, hovered);
387
388 let content_rect = rect.shrink2(Vec2::new(padding_x, 0.0));
390 let mut child_ui = ui.new_child(
391 egui::UiBuilder::new()
392 .max_rect(content_rect)
393 .layout(egui::Layout::left_to_right(egui::Align::Center)),
394 );
395 child_ui.style_mut().visuals.override_text_color = Some(text_color);
396
397 let ctx = ContentContext {
398 color: text_color,
399 font_size: self.size.font_size(&theme.typography),
400 is_active: false,
401 };
402 content(&mut child_ui, &ctx);
403 }
404
405 response
406 }
407}
408
409pub use ButtonVariant as Variant;
411
412#[allow(non_upper_case_globals)]
414impl ButtonVariant {
415 pub const Filled: Self = Self::Default;
417 pub const Outlined: Self = Self::Outline;
419 pub const Text: Self = Self::Ghost;
421 pub const FilledTonal: Self = Self::Secondary;
423 pub const Elevated: Self = Self::Secondary;
425}