1use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
2use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
3use smallvec::SmallVec;
4
5use crate::{prelude::*, DynamicSpacing, Elevation};
6
7/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
8pub trait SelectableButton: Toggleable {
9 fn selected_style(self, style: ButtonStyle) -> Self;
10}
11
12/// A common set of traits all buttons must implement.
13pub trait ButtonCommon: Clickable + Disableable {
14 /// A unique element ID to identify the button.
15 fn id(&self) -> &ElementId;
16
17 /// The visual style of the button.
18 ///
19 /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
20 /// for an emphasized button.
21 fn style(self, style: ButtonStyle) -> Self;
22
23 /// The size of the button.
24 ///
25 /// Most buttons will use the default size.
26 ///
27 /// [`ButtonSize`] can also be used to help build non-button elements
28 /// that are consistently sized with buttons.
29 fn size(self, size: ButtonSize) -> Self;
30
31 /// The tooltip that shows when a user hovers over the button.
32 ///
33 /// Nearly all interactable elements should have a tooltip. Some example
34 /// exceptions might a scroll bar, or a slider.
35 fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self;
36
37 fn elevation(self, elevation: Elevation) -> Self;
38}
39
40#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
41pub enum IconPosition {
42 #[default]
43 Start,
44 End,
45}
46
47#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
48pub enum KeybindingPosition {
49 Start,
50 #[default]
51 End,
52}
53
54#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
55pub enum TintColor {
56 #[default]
57 Accent,
58 Error,
59 Warning,
60 Success,
61}
62
63impl TintColor {
64 fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles {
65 match self {
66 TintColor::Accent => ButtonLikeStyles {
67 background: cx.theme().status().info_background,
68 border_color: cx.theme().status().info_border,
69 label_color: cx.theme().colors().text,
70 icon_color: cx.theme().colors().text,
71 },
72 TintColor::Error => ButtonLikeStyles {
73 background: cx.theme().status().error_background,
74 border_color: cx.theme().status().error_border,
75 label_color: cx.theme().colors().text,
76 icon_color: cx.theme().colors().text,
77 },
78 TintColor::Warning => ButtonLikeStyles {
79 background: cx.theme().status().warning_background,
80 border_color: cx.theme().status().warning_border,
81 label_color: cx.theme().colors().text,
82 icon_color: cx.theme().colors().text,
83 },
84 TintColor::Success => ButtonLikeStyles {
85 background: cx.theme().status().success_background,
86 border_color: cx.theme().status().success_border,
87 label_color: cx.theme().colors().text,
88 icon_color: cx.theme().colors().text,
89 },
90 }
91 }
92}
93
94impl From<TintColor> for Color {
95 fn from(tint: TintColor) -> Self {
96 match tint {
97 TintColor::Accent => Color::Accent,
98 TintColor::Error => Color::Error,
99 TintColor::Warning => Color::Warning,
100 TintColor::Success => Color::Success,
101 }
102 }
103}
104
105// Used to go from ButtonStyle -> Color through tint colors.
106impl From<ButtonStyle> for Color {
107 fn from(style: ButtonStyle) -> Self {
108 match style {
109 ButtonStyle::Tinted(tint) => tint.into(),
110 _ => Color::Default,
111 }
112 }
113}
114
115/// The visual appearance of a button.
116#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
117pub enum ButtonStyle {
118 /// A filled button with a solid background color. Provides emphasis versus
119 /// the more common subtle button.
120 Filled,
121
122 /// Used to emphasize a button in some way, like a selected state, or a semantic
123 /// coloring like an error or success button.
124 Tinted(TintColor),
125
126 /// The default button style, used for most buttons. Has a transparent background,
127 /// but has a background color to indicate states like hover and active.
128 #[default]
129 Subtle,
130
131 /// Used for buttons that only change foreground color on hover and active states.
132 ///
133 /// TODO: Better docs for this.
134 Transparent,
135}
136
137#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
138pub(crate) enum ButtonLikeRounding {
139 All,
140 Left,
141 Right,
142}
143
144#[derive(Debug, Clone)]
145pub(crate) struct ButtonLikeStyles {
146 pub background: Hsla,
147 #[allow(unused)]
148 pub border_color: Hsla,
149 #[allow(unused)]
150 pub label_color: Hsla,
151 #[allow(unused)]
152 pub icon_color: Hsla,
153}
154
155fn element_bg_from_elevation(elevation: Option<Elevation>, cx: &mut App) -> Hsla {
156 match elevation {
157 Some(Elevation::Background) => cx.theme().colors().element_background,
158 Some(Elevation::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
159 Some(Elevation::Surface) => cx.theme().colors().surface_background,
160 Some(Elevation::ModalSurface) => cx.theme().colors().background,
161 _ => cx.theme().colors().element_background,
162 }
163}
164
165impl ButtonStyle {
166 pub(crate) fn enabled(self, elevation: Option<Elevation>, cx: &mut App) -> ButtonLikeStyles {
167 match self {
168 ButtonStyle::Filled => ButtonLikeStyles {
169 background: element_bg_from_elevation(elevation, cx),
170 border_color: transparent_black(),
171 label_color: Color::Default.color(cx),
172 icon_color: Color::Default.color(cx),
173 },
174 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
175 ButtonStyle::Subtle => ButtonLikeStyles {
176 background: cx.theme().colors().ghost_element_background,
177 border_color: transparent_black(),
178 label_color: Color::Default.color(cx),
179 icon_color: Color::Default.color(cx),
180 },
181 ButtonStyle::Transparent => ButtonLikeStyles {
182 background: transparent_black(),
183 border_color: transparent_black(),
184 label_color: Color::Default.color(cx),
185 icon_color: Color::Default.color(cx),
186 },
187 }
188 }
189
190 pub(crate) fn hovered(self, elevation: Option<Elevation>, cx: &mut App) -> ButtonLikeStyles {
191 match self {
192 ButtonStyle::Filled => {
193 let mut filled_background = element_bg_from_elevation(elevation, cx);
194 filled_background.fade_out(0.92);
195
196 ButtonLikeStyles {
197 background: filled_background,
198 border_color: transparent_black(),
199 label_color: Color::Default.color(cx),
200 icon_color: Color::Default.color(cx),
201 }
202 }
203 ButtonStyle::Tinted(tint) => {
204 let mut styles = tint.button_like_style(cx);
205 let theme = cx.theme();
206 styles.background = theme.darken(styles.background, 0.05, 0.2);
207 styles
208 }
209 ButtonStyle::Subtle => ButtonLikeStyles {
210 background: cx.theme().colors().ghost_element_hover,
211 border_color: transparent_black(),
212 label_color: Color::Default.color(cx),
213 icon_color: Color::Default.color(cx),
214 },
215 ButtonStyle::Transparent => ButtonLikeStyles {
216 background: transparent_black(),
217 border_color: transparent_black(),
218 // TODO: These are not great
219 label_color: Color::Muted.color(cx),
220 // TODO: These are not great
221 icon_color: Color::Muted.color(cx),
222 },
223 }
224 }
225
226 pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
227 match self {
228 ButtonStyle::Filled => ButtonLikeStyles {
229 background: cx.theme().colors().element_active,
230 border_color: transparent_black(),
231 label_color: Color::Default.color(cx),
232 icon_color: Color::Default.color(cx),
233 },
234 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
235 ButtonStyle::Subtle => ButtonLikeStyles {
236 background: cx.theme().colors().ghost_element_active,
237 border_color: transparent_black(),
238 label_color: Color::Default.color(cx),
239 icon_color: Color::Default.color(cx),
240 },
241 ButtonStyle::Transparent => ButtonLikeStyles {
242 background: transparent_black(),
243 border_color: transparent_black(),
244 // TODO: These are not great
245 label_color: Color::Muted.color(cx),
246 // TODO: These are not great
247 icon_color: Color::Muted.color(cx),
248 },
249 }
250 }
251
252 #[allow(unused)]
253 pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
254 match self {
255 ButtonStyle::Filled => ButtonLikeStyles {
256 background: cx.theme().colors().element_background,
257 border_color: cx.theme().colors().border_focused,
258 label_color: Color::Default.color(cx),
259 icon_color: Color::Default.color(cx),
260 },
261 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
262 ButtonStyle::Subtle => ButtonLikeStyles {
263 background: cx.theme().colors().ghost_element_background,
264 border_color: cx.theme().colors().border_focused,
265 label_color: Color::Default.color(cx),
266 icon_color: Color::Default.color(cx),
267 },
268 ButtonStyle::Transparent => ButtonLikeStyles {
269 background: transparent_black(),
270 border_color: cx.theme().colors().border_focused,
271 label_color: Color::Accent.color(cx),
272 icon_color: Color::Accent.color(cx),
273 },
274 }
275 }
276
277 #[allow(unused)]
278 pub(crate) fn disabled(
279 self,
280 elevation: Option<Elevation>,
281 window: &mut Window,
282 cx: &mut App,
283 ) -> ButtonLikeStyles {
284 match self {
285 ButtonStyle::Filled => ButtonLikeStyles {
286 background: cx.theme().colors().element_disabled,
287 border_color: cx.theme().colors().border_disabled,
288 label_color: Color::Disabled.color(cx),
289 icon_color: Color::Disabled.color(cx),
290 },
291 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
292 ButtonStyle::Subtle => ButtonLikeStyles {
293 background: cx.theme().colors().ghost_element_disabled,
294 border_color: cx.theme().colors().border_disabled,
295 label_color: Color::Disabled.color(cx),
296 icon_color: Color::Disabled.color(cx),
297 },
298 ButtonStyle::Transparent => ButtonLikeStyles {
299 background: transparent_black(),
300 border_color: transparent_black(),
301 label_color: Color::Disabled.color(cx),
302 icon_color: Color::Disabled.color(cx),
303 },
304 }
305 }
306}
307
308/// The height of a button.
309///
310/// Can also be used to size non-button elements to align with [`Button`]s.
311#[derive(Default, PartialEq, Clone, Copy)]
312pub enum ButtonSize {
313 Large,
314 #[default]
315 Default,
316 Compact,
317 None,
318}
319
320impl ButtonSize {
321 pub fn rems(self) -> Rems {
322 match self {
323 ButtonSize::Large => rems_from_px(32.),
324 ButtonSize::Default => rems_from_px(22.),
325 ButtonSize::Compact => rems_from_px(18.),
326 ButtonSize::None => rems_from_px(16.),
327 }
328 }
329}
330
331/// A button-like element that can be used to create a custom button when
332/// prebuilt buttons are not sufficient. Use this sparingly, as it is
333/// unconstrained and may make the UI feel less consistent.
334///
335/// This is also used to build the prebuilt buttons.
336#[derive(IntoElement)]
337pub struct ButtonLike {
338 pub(super) base: Div,
339 id: ElementId,
340 pub(super) style: ButtonStyle,
341 pub(super) disabled: bool,
342 pub(super) selected: bool,
343 pub(super) selected_style: Option<ButtonStyle>,
344 pub(super) width: Option<DefiniteLength>,
345 pub(super) height: Option<DefiniteLength>,
346 pub(super) layer: Option<Elevation>,
347 size: ButtonSize,
348 rounding: Option<ButtonLikeRounding>,
349 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
350 cursor_style: CursorStyle,
351 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
352 children: SmallVec<[AnyElement; 2]>,
353}
354
355impl ButtonLike {
356 pub fn new(id: impl Into<ElementId>) -> Self {
357 Self {
358 base: div(),
359 id: id.into(),
360 style: ButtonStyle::default(),
361 disabled: false,
362 selected: false,
363 selected_style: None,
364 width: None,
365 height: None,
366 size: ButtonSize::Default,
367 rounding: Some(ButtonLikeRounding::All),
368 tooltip: None,
369 children: SmallVec::new(),
370 cursor_style: CursorStyle::PointingHand,
371 on_click: None,
372 layer: None,
373 }
374 }
375
376 pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
377 Self::new(id).rounding(ButtonLikeRounding::Left)
378 }
379
380 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
381 Self::new(id).rounding(ButtonLikeRounding::Right)
382 }
383
384 pub fn opacity(mut self, opacity: f32) -> Self {
385 self.base = self.base.opacity(opacity);
386 self
387 }
388
389 pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
390 self.height = Some(height);
391 self
392 }
393
394 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
395 self.rounding = rounding.into();
396 self
397 }
398}
399
400impl Disableable for ButtonLike {
401 fn disabled(mut self, disabled: bool) -> Self {
402 self.disabled = disabled;
403 self
404 }
405}
406
407impl Toggleable for ButtonLike {
408 fn toggle_state(mut self, selected: bool) -> Self {
409 self.selected = selected;
410 self
411 }
412}
413
414impl SelectableButton for ButtonLike {
415 fn selected_style(mut self, style: ButtonStyle) -> Self {
416 self.selected_style = Some(style);
417 self
418 }
419}
420
421impl Clickable for ButtonLike {
422 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
423 self.on_click = Some(Box::new(handler));
424 self
425 }
426
427 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
428 self.cursor_style = cursor_style;
429 self
430 }
431}
432
433impl FixedWidth for ButtonLike {
434 fn width(mut self, width: DefiniteLength) -> Self {
435 self.width = Some(width);
436 self
437 }
438
439 fn full_width(mut self) -> Self {
440 self.width = Some(relative(1.));
441 self
442 }
443}
444
445impl ButtonCommon for ButtonLike {
446 fn id(&self) -> &ElementId {
447 &self.id
448 }
449
450 fn style(mut self, style: ButtonStyle) -> Self {
451 self.style = style;
452 self
453 }
454
455 fn size(mut self, size: ButtonSize) -> Self {
456 self.size = size;
457 self
458 }
459
460 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
461 self.tooltip = Some(Box::new(tooltip));
462 self
463 }
464
465 fn elevation(mut self, elevation: Elevation) -> Self {
466 self.layer = Some(elevation);
467 self
468 }
469}
470
471impl VisibleOnHover for ButtonLike {
472 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
473 self.base = self.base.visible_on_hover(group_name);
474 self
475 }
476}
477
478impl ParentElement for ButtonLike {
479 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
480 self.children.extend(elements)
481 }
482}
483
484impl RenderOnce for ButtonLike {
485 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
486 let style = self
487 .selected_style
488 .filter(|_| self.selected)
489 .unwrap_or(self.style);
490
491 self.base
492 .h_flex()
493 .id(self.id.clone())
494 .font_ui(cx)
495 .group("")
496 .flex_none()
497 .h(self.height.unwrap_or(self.size.rems().into()))
498 .when_some(self.width, |this, width| {
499 this.w(width).justify_center().text_center()
500 })
501 .when_some(self.rounding, |this, rounding| match rounding {
502 ButtonLikeRounding::All => this.rounded_sm(),
503 ButtonLikeRounding::Left => this.rounded_l_sm(),
504 ButtonLikeRounding::Right => this.rounded_r_sm(),
505 })
506 .gap(DynamicSpacing::Base04.rems(cx))
507 .map(|this| match self.size {
508 ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
509 ButtonSize::Default | ButtonSize::Compact => {
510 this.px(DynamicSpacing::Base04.rems(cx))
511 }
512 ButtonSize::None => this,
513 })
514 .bg(style.enabled(self.layer, cx).background)
515 .when(self.disabled, |this| this.cursor_not_allowed())
516 .when(!self.disabled, |this| {
517 this.cursor_pointer()
518 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
519 .active(|active| active.bg(style.active(cx).background))
520 })
521 .when_some(
522 self.on_click.filter(|_| !self.disabled),
523 |this, on_click| {
524 this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
525 .on_click(move |event, window, cx| {
526 cx.stop_propagation();
527 (on_click)(event, window, cx)
528 })
529 },
530 )
531 .when_some(self.tooltip, |this, tooltip| {
532 this.tooltip(move |window, cx| tooltip(window, cx))
533 })
534 .children(self.children)
535 }
536}