1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ButtonVariant {
11 #[default]
13 Primary,
14 Secondary,
16 Destructive,
18 Ghost,
20 Outline,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum ButtonSize {
27 Xs,
29 Sm,
31 #[default]
33 Md,
34 Lg,
36}
37
38#[derive(Debug, Clone)]
40pub struct ButtonTheme {
41 pub accent: Rgba,
42 pub accent_hover: Rgba,
43 pub surface: Rgba,
44 pub surface_hover: Rgba,
45 pub text_primary: Rgba,
46 pub text_secondary: Rgba,
47 pub error: Rgba,
48 pub border: Rgba,
49}
50
51impl Default for ButtonTheme {
52 fn default() -> Self {
53 Self {
54 accent: rgb(0x007acc),
55 accent_hover: rgb(0x0098ff),
56 surface: rgb(0x3c3c3c),
57 surface_hover: rgb(0x4a4a4a),
58 text_primary: rgb(0xffffff),
59 text_secondary: rgb(0xcccccc),
60 error: rgb(0xcc3333),
61 border: rgb(0x555555),
62 }
63 }
64}
65
66#[derive(IntoElement)]
68pub struct Button {
69 id: ElementId,
70 label: SharedString,
71 variant: ButtonVariant,
72 size: ButtonSize,
73 disabled: bool,
74 selected: bool,
75 full_width: bool,
76 icon_left: Option<SharedString>,
77 icon_right: Option<SharedString>,
78 theme: Option<ButtonTheme>,
79 on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
80}
81
82impl Button {
83 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
85 Self {
86 id: id.into(),
87 label: label.into(),
88 variant: ButtonVariant::default(),
89 size: ButtonSize::default(),
90 disabled: false,
91 selected: false,
92 full_width: false,
93 icon_left: None,
94 icon_right: None,
95 theme: None,
96 on_click: None,
97 }
98 }
99
100 pub fn variant(mut self, variant: ButtonVariant) -> Self {
102 self.variant = variant;
103 self
104 }
105
106 pub fn size(mut self, size: ButtonSize) -> Self {
108 self.size = size;
109 self
110 }
111
112 pub fn disabled(mut self, disabled: bool) -> Self {
114 self.disabled = disabled;
115 self
116 }
117
118 pub fn selected(mut self, selected: bool) -> Self {
120 self.selected = selected;
121 self
122 }
123
124 pub fn full_width(mut self, full_width: bool) -> Self {
126 self.full_width = full_width;
127 self
128 }
129
130 pub fn icon_left(mut self, icon: impl Into<SharedString>) -> Self {
132 self.icon_left = Some(icon.into());
133 self
134 }
135
136 pub fn icon_right(mut self, icon: impl Into<SharedString>) -> Self {
138 self.icon_right = Some(icon.into());
139 self
140 }
141
142 pub fn theme(mut self, theme: ButtonTheme) -> Self {
144 self.theme = Some(theme);
145 self
146 }
147
148 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
150 self.on_click = Some(Box::new(handler));
151 self
152 }
153
154 pub fn build(self) -> Stateful<Div> {
157 let theme = self.theme.unwrap_or_default();
158
159 let (bg, bg_hover, text_color, border_color) = if self.selected {
161 (
162 theme.accent,
163 theme.accent_hover,
164 theme.text_primary,
165 theme.accent,
166 )
167 } else {
168 match self.variant {
169 ButtonVariant::Primary => (
170 theme.accent,
171 theme.accent_hover,
172 theme.text_primary,
173 theme.accent,
174 ),
175 ButtonVariant::Secondary => (
176 theme.surface,
177 theme.surface_hover,
178 theme.text_secondary,
179 theme.surface,
180 ),
181 ButtonVariant::Destructive => {
182 (theme.error, rgb(0xe64545), theme.text_primary, theme.error)
183 }
184 ButtonVariant::Ghost => (
185 rgba(0x00000000),
186 theme.surface_hover,
187 theme.text_secondary,
188 rgba(0x00000000),
189 ),
190 ButtonVariant::Outline => (
191 rgba(0x00000000),
192 theme.surface,
193 theme.text_secondary,
194 theme.border,
195 ),
196 }
197 };
198
199 let (px_val, py_val) = match self.size {
200 ButtonSize::Xs => (px(6.0), px(2.0)),
201 ButtonSize::Sm => (px(8.0), px(4.0)),
202 ButtonSize::Md => (px(12.0), px(6.0)),
203 ButtonSize::Lg => (px(24.0), px(12.0)),
204 };
205
206 let mut el = div()
207 .id(self.id)
208 .flex()
209 .items_center()
210 .justify_center()
211 .gap_2()
212 .px(px_val)
213 .py(py_val)
214 .rounded_md()
215 .bg(bg)
216 .text_color(text_color)
217 .border_1()
218 .border_color(border_color);
219
220 el = match self.size {
222 ButtonSize::Xs => el.text_xs(),
223 ButtonSize::Sm => el.text_xs(),
224 ButtonSize::Md => el.text_sm(),
225 ButtonSize::Lg => el.text_lg(),
226 };
227
228 if self.full_width {
230 el = el.w_full();
231 }
232
233 if self.disabled {
234 el = el.opacity(0.5).cursor_not_allowed();
235 } else {
236 el = el.cursor_pointer().hover(|style| style.bg(bg_hover));
237 }
238
239 if let Some(icon) = self.icon_left {
241 el = el.child(div().child(icon));
242 }
243
244 el = el.child(self.label);
246
247 if let Some(icon) = self.icon_right {
249 el = el.child(div().child(icon));
250 }
251
252 el
253 }
254}
255
256impl RenderOnce for Button {
257 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
258 let theme = self.theme.unwrap_or_default();
259
260 let (bg, bg_hover, text_color, border_color) = if self.selected {
262 (
263 theme.accent,
264 theme.accent_hover,
265 theme.text_primary,
266 theme.accent,
267 )
268 } else {
269 match self.variant {
270 ButtonVariant::Primary => (
271 theme.accent,
272 theme.accent_hover,
273 theme.text_primary,
274 theme.accent,
275 ),
276 ButtonVariant::Secondary => (
277 theme.surface,
278 theme.surface_hover,
279 theme.text_secondary,
280 theme.surface,
281 ),
282 ButtonVariant::Destructive => {
283 (theme.error, rgb(0xe64545), theme.text_primary, theme.error)
284 }
285 ButtonVariant::Ghost => (
286 rgba(0x00000000),
287 theme.surface_hover,
288 theme.text_secondary,
289 rgba(0x00000000),
290 ),
291 ButtonVariant::Outline => (
292 rgba(0x00000000),
293 theme.surface,
294 theme.text_secondary,
295 theme.border,
296 ),
297 }
298 };
299
300 let (px_val, py_val) = match self.size {
301 ButtonSize::Xs => (px(6.0), px(2.0)),
302 ButtonSize::Sm => (px(8.0), px(4.0)),
303 ButtonSize::Md => (px(12.0), px(6.0)),
304 ButtonSize::Lg => (px(24.0), px(12.0)),
305 };
306
307 let mut el = div()
308 .id(self.id)
309 .flex()
310 .items_center()
311 .justify_center()
312 .gap_2()
313 .px(px_val)
314 .py(py_val)
315 .rounded_md()
316 .bg(bg)
317 .text_color(text_color)
318 .border_1()
319 .border_color(border_color)
320 .cursor_pointer();
321
322 el = match self.size {
324 ButtonSize::Xs => el.text_xs(),
325 ButtonSize::Sm => el.text_xs(),
326 ButtonSize::Md => el.text_sm(),
327 ButtonSize::Lg => el.text_lg(),
328 };
329
330 if self.full_width {
332 el = el.w_full();
333 }
334
335 if self.disabled {
336 el = el.opacity(0.5).cursor_not_allowed();
337 } else {
338 el = el.hover(|style| style.bg(bg_hover));
339
340 if let Some(handler) = self.on_click {
341 el = el.on_mouse_up(MouseButton::Left, move |_event, window, cx| {
342 handler(window, cx);
343 });
344 }
345 }
346
347 if let Some(icon) = self.icon_left {
349 el = el.child(div().child(icon));
350 }
351
352 el = el.child(self.label);
354
355 if let Some(icon) = self.icon_right {
357 el = el.child(div().child(icon));
358 }
359
360 el
361 }
362}