1use crate::common::{Size, Variant};
8use egui::{Color32, FontId, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetText};
9use egui_components_theme::{mix, Theme, ThemeColor};
10
11pub struct Button {
12 label: WidgetText,
13 variant: Variant,
14 size: Size,
15 disabled: bool,
16 full_width: bool,
17 min_width: Option<f32>,
18}
19
20impl Button {
21 pub fn new(label: impl Into<WidgetText>) -> Self {
22 Self {
23 label: label.into(),
24 variant: Variant::Primary,
25 size: Size::Medium,
26 disabled: false,
27 full_width: false,
28 min_width: None,
29 }
30 }
31
32 pub fn primary(label: impl Into<WidgetText>) -> Self {
33 Self::new(label).variant(Variant::Primary)
34 }
35 pub fn secondary(label: impl Into<WidgetText>) -> Self {
36 Self::new(label).variant(Variant::Secondary)
37 }
38 pub fn ghost(label: impl Into<WidgetText>) -> Self {
39 Self::new(label).variant(Variant::Ghost)
40 }
41 pub fn outline(label: impl Into<WidgetText>) -> Self {
42 Self::new(label).variant(Variant::Outline)
43 }
44 pub fn danger(label: impl Into<WidgetText>) -> Self {
45 Self::new(label).variant(Variant::Danger)
46 }
47 pub fn link(label: impl Into<WidgetText>) -> Self {
48 Self::new(label).variant(Variant::Link)
49 }
50
51 pub fn variant(mut self, v: Variant) -> Self {
52 self.variant = v;
53 self
54 }
55 pub fn size(mut self, s: Size) -> Self {
56 self.size = s;
57 self
58 }
59 pub fn small(self) -> Self {
60 self.size(Size::Small)
61 }
62 pub fn large(self) -> Self {
63 self.size(Size::Large)
64 }
65 pub fn disabled(mut self, d: bool) -> Self {
66 self.disabled = d;
67 self
68 }
69 pub fn full_width(mut self) -> Self {
70 self.full_width = true;
71 self
72 }
73 pub fn min_width(mut self, w: f32) -> Self {
74 self.min_width = Some(w);
75 self
76 }
77}
78
79impl Widget for Button {
80 fn ui(self, ui: &mut Ui) -> Response {
81 let theme = Theme::get(ui.ctx());
82 let m = theme.metrics;
83 let height = self.size.button_height(&m);
84 let pad_x = self.size.button_padding_x(&m);
85 let font = FontId::proportional(self.size.font_size(&m));
86
87 let galley = self.label.clone().into_galley(
88 ui,
89 Some(egui::TextWrapMode::Extend),
90 f32::INFINITY,
91 font,
92 );
93 let text_w = galley.size().x;
94
95 let desired_w = if self.full_width {
96 ui.available_width()
97 } else {
98 (text_w + pad_x * 2.0).max(self.min_width.unwrap_or(0.0))
99 };
100 let desired_size = Vec2::new(desired_w, height);
101
102 let sense = if self.disabled { Sense::hover() } else { Sense::click() };
103 let (rect, response) = ui.allocate_exact_size(desired_size, sense);
104
105 if ui.is_rect_visible(rect) {
106 paint_button(ui, rect, &response, &theme, self.variant, self.disabled, &galley);
107 }
108
109 response
110 }
111}
112
113fn paint_button(
114 ui: &mut Ui,
115 rect: Rect,
116 response: &Response,
117 theme: &Theme,
118 variant: Variant,
119 disabled: bool,
120 galley: &std::sync::Arc<egui::Galley>,
121) {
122 let c = &theme.colors;
123 let radius = theme.corner();
124
125 let (bg, fg, border) = variant_colors(c, variant);
126
127 let state_bg = if disabled {
128 mix(bg, Color32::TRANSPARENT, 0.5)
129 } else if response.is_pointer_button_down_on() {
130 match variant {
131 Variant::Primary => c.primary_active_background,
132 Variant::Secondary => c.secondary_active_background,
133 Variant::Danger => darken(c.danger_background, 0.15),
134 Variant::Success => darken(c.success_background, 0.15),
135 Variant::Warning => darken(c.warning_background, 0.15),
136 Variant::Info => darken(c.info_background, 0.15),
137 Variant::Ghost | Variant::Outline => mix(c.accent_background, c.foreground, 0.05),
138 Variant::Link => bg,
139 }
140 } else if response.hovered() {
141 match variant {
142 Variant::Primary => c.primary_hover_background,
143 Variant::Secondary => c.secondary_hover_background,
144 Variant::Danger => lighten(c.danger_background, 0.08),
145 Variant::Success => lighten(c.success_background, 0.08),
146 Variant::Warning => lighten(c.warning_background, 0.08),
147 Variant::Info => lighten(c.info_background, 0.08),
148 Variant::Ghost | Variant::Outline => c.accent_background,
149 Variant::Link => bg,
150 }
151 } else {
152 bg
153 };
154
155 let painter = ui.painter();
156
157 if !matches!(variant, Variant::Link | Variant::Ghost) || response.hovered() || response.is_pointer_button_down_on() {
158 painter.rect_filled(rect, radius, state_bg);
159 }
160
161 if let Some(stroke) = border {
162 painter.rect_stroke(rect, radius, stroke, egui::StrokeKind::Inside);
163 }
164
165 if response.has_focus() {
167 let ring_rect = rect.expand(2.0);
168 painter.rect_stroke(
169 ring_rect,
170 theme.corner(),
171 theme.focus_ring(),
172 egui::StrokeKind::Outside,
173 );
174 }
175
176 let text_color = if disabled { mix(fg, c.muted_foreground, 0.5) } else { fg };
178 let text_pos = rect.center();
179 painter.galley_with_override_text_color(
180 text_pos - galley.size() * 0.5,
181 galley.clone(),
182 text_color,
183 );
184
185 if matches!(variant, Variant::Link) && response.hovered() {
187 let underline_y = text_pos.y + galley.size().y * 0.5 - 1.0;
188 painter.line_segment(
189 [
190 egui::pos2(rect.center().x - galley.size().x * 0.5, underline_y),
191 egui::pos2(rect.center().x + galley.size().x * 0.5, underline_y),
192 ],
193 Stroke::new(1.0, text_color),
194 );
195 }
196
197 if !disabled && response.hovered() {
199 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
200 }
201}
202
203fn variant_colors(c: &ThemeColor, variant: Variant) -> (Color32, Color32, Option<Stroke>) {
204 match variant {
205 Variant::Primary => (c.primary_background, c.primary_foreground, None),
206 Variant::Secondary => (c.secondary_background, c.secondary_foreground, None),
207 Variant::Ghost => (Color32::TRANSPARENT, c.foreground, None),
208 Variant::Outline => (
209 Color32::TRANSPARENT,
210 c.foreground,
211 Some(Stroke::new(1.0, c.border)),
212 ),
213 Variant::Link => (Color32::TRANSPARENT, c.link_foreground, None),
214 Variant::Danger => (c.danger_background, c.danger_foreground, None),
215 Variant::Success => (c.success_background, c.success_foreground, None),
216 Variant::Warning => (c.warning_background, c.warning_foreground, None),
217 Variant::Info => (c.info_background, c.info_foreground, None),
218 }
219}
220
221fn darken(c: Color32, t: f32) -> Color32 {
222 mix(c, Color32::BLACK, t)
223}
224fn lighten(c: Color32, t: f32) -> Color32 {
225 mix(c, Color32::WHITE, t)
226}