1use gpui::{AnyElement, AnyView, ClickEvent, Hsla, Rems, transparent_black};
2use gpui::{CursorStyle, DefiniteLength, MouseButton, MouseDownEvent, MouseUpEvent, relative};
3use smallvec::SmallVec;
4
5use crate::{DynamicSpacing, ElevationIndex, prelude::*};
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 layer(self, elevation: ElevationIndex) -> 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<ElevationIndex>, cx: &mut App) -> Hsla {
156 match elevation {
157 Some(ElevationIndex::Background) => cx.theme().colors().element_background,
158 Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
159 Some(ElevationIndex::Surface) => cx.theme().colors().surface_background,
160 Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
161 _ => cx.theme().colors().element_background,
162 }
163}
164
165impl ButtonStyle {
166 pub(crate) fn enabled(
167 self,
168 elevation: Option<ElevationIndex>,
169
170 cx: &mut App,
171 ) -> ButtonLikeStyles {
172 match self {
173 ButtonStyle::Filled => ButtonLikeStyles {
174 background: element_bg_from_elevation(elevation, cx),
175 border_color: transparent_black(),
176 label_color: Color::Default.color(cx),
177 icon_color: Color::Default.color(cx),
178 },
179 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
180 ButtonStyle::Subtle => ButtonLikeStyles {
181 background: cx.theme().colors().ghost_element_background,
182 border_color: transparent_black(),
183 label_color: Color::Default.color(cx),
184 icon_color: Color::Default.color(cx),
185 },
186 ButtonStyle::Transparent => ButtonLikeStyles {
187 background: transparent_black(),
188 border_color: transparent_black(),
189 label_color: Color::Default.color(cx),
190 icon_color: Color::Default.color(cx),
191 },
192 }
193 }
194
195 pub(crate) fn hovered(
196 self,
197 elevation: Option<ElevationIndex>,
198
199 cx: &mut App,
200 ) -> ButtonLikeStyles {
201 match self {
202 ButtonStyle::Filled => {
203 let mut filled_background = element_bg_from_elevation(elevation, cx);
204 filled_background.fade_out(0.92);
205
206 ButtonLikeStyles {
207 background: filled_background,
208 border_color: transparent_black(),
209 label_color: Color::Default.color(cx),
210 icon_color: Color::Default.color(cx),
211 }
212 }
213 ButtonStyle::Tinted(tint) => {
214 let mut styles = tint.button_like_style(cx);
215 let theme = cx.theme();
216 styles.background = theme.darken(styles.background, 0.05, 0.2);
217 styles
218 }
219 ButtonStyle::Subtle => ButtonLikeStyles {
220 background: cx.theme().colors().ghost_element_hover,
221 border_color: transparent_black(),
222 label_color: Color::Default.color(cx),
223 icon_color: Color::Default.color(cx),
224 },
225 ButtonStyle::Transparent => ButtonLikeStyles {
226 background: transparent_black(),
227 border_color: transparent_black(),
228 // TODO: These are not great
229 label_color: Color::Muted.color(cx),
230 // TODO: These are not great
231 icon_color: Color::Muted.color(cx),
232 },
233 }
234 }
235
236 pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
237 match self {
238 ButtonStyle::Filled => ButtonLikeStyles {
239 background: cx.theme().colors().element_active,
240 border_color: transparent_black(),
241 label_color: Color::Default.color(cx),
242 icon_color: Color::Default.color(cx),
243 },
244 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
245 ButtonStyle::Subtle => ButtonLikeStyles {
246 background: cx.theme().colors().ghost_element_active,
247 border_color: transparent_black(),
248 label_color: Color::Default.color(cx),
249 icon_color: Color::Default.color(cx),
250 },
251 ButtonStyle::Transparent => ButtonLikeStyles {
252 background: transparent_black(),
253 border_color: transparent_black(),
254 // TODO: These are not great
255 label_color: Color::Muted.color(cx),
256 // TODO: These are not great
257 icon_color: Color::Muted.color(cx),
258 },
259 }
260 }
261
262 #[allow(unused)]
263 pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
264 match self {
265 ButtonStyle::Filled => ButtonLikeStyles {
266 background: cx.theme().colors().element_background,
267 border_color: cx.theme().colors().border_focused,
268 label_color: Color::Default.color(cx),
269 icon_color: Color::Default.color(cx),
270 },
271 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
272 ButtonStyle::Subtle => ButtonLikeStyles {
273 background: cx.theme().colors().ghost_element_background,
274 border_color: cx.theme().colors().border_focused,
275 label_color: Color::Default.color(cx),
276 icon_color: Color::Default.color(cx),
277 },
278 ButtonStyle::Transparent => ButtonLikeStyles {
279 background: transparent_black(),
280 border_color: cx.theme().colors().border_focused,
281 label_color: Color::Accent.color(cx),
282 icon_color: Color::Accent.color(cx),
283 },
284 }
285 }
286
287 #[allow(unused)]
288 pub(crate) fn disabled(
289 self,
290 elevation: Option<ElevationIndex>,
291 window: &mut Window,
292 cx: &mut App,
293 ) -> ButtonLikeStyles {
294 match self {
295 ButtonStyle::Filled => ButtonLikeStyles {
296 background: cx.theme().colors().element_disabled,
297 border_color: cx.theme().colors().border_disabled,
298 label_color: Color::Disabled.color(cx),
299 icon_color: Color::Disabled.color(cx),
300 },
301 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
302 ButtonStyle::Subtle => ButtonLikeStyles {
303 background: cx.theme().colors().ghost_element_disabled,
304 border_color: cx.theme().colors().border_disabled,
305 label_color: Color::Disabled.color(cx),
306 icon_color: Color::Disabled.color(cx),
307 },
308 ButtonStyle::Transparent => ButtonLikeStyles {
309 background: transparent_black(),
310 border_color: transparent_black(),
311 label_color: Color::Disabled.color(cx),
312 icon_color: Color::Disabled.color(cx),
313 },
314 }
315 }
316}
317
318/// The height of a button.
319///
320/// Can also be used to size non-button elements to align with [`Button`]s.
321#[derive(Default, PartialEq, Clone, Copy)]
322pub enum ButtonSize {
323 Large,
324 #[default]
325 Default,
326 Compact,
327 None,
328}
329
330impl ButtonSize {
331 pub fn rems(self) -> Rems {
332 match self {
333 ButtonSize::Large => rems_from_px(32.),
334 ButtonSize::Default => rems_from_px(22.),
335 ButtonSize::Compact => rems_from_px(18.),
336 ButtonSize::None => rems_from_px(16.),
337 }
338 }
339}
340
341/// A button-like element that can be used to create a custom button when
342/// prebuilt buttons are not sufficient. Use this sparingly, as it is
343/// unconstrained and may make the UI feel less consistent.
344///
345/// This is also used to build the prebuilt buttons.
346#[derive(IntoElement)]
347pub struct ButtonLike {
348 pub(super) base: Div,
349 id: ElementId,
350 pub(super) style: ButtonStyle,
351 pub(super) disabled: bool,
352 pub(super) selected: bool,
353 pub(super) selected_style: Option<ButtonStyle>,
354 pub(super) width: Option<DefiniteLength>,
355 pub(super) height: Option<DefiniteLength>,
356 pub(super) layer: Option<ElevationIndex>,
357 size: ButtonSize,
358 rounding: Option<ButtonLikeRounding>,
359 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
360 cursor_style: CursorStyle,
361 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
362 on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
363 children: SmallVec<[AnyElement; 2]>,
364}
365
366impl ButtonLike {
367 pub fn new(id: impl Into<ElementId>) -> Self {
368 Self {
369 base: div(),
370 id: id.into(),
371 style: ButtonStyle::default(),
372 disabled: false,
373 selected: false,
374 selected_style: None,
375 width: None,
376 height: None,
377 size: ButtonSize::Default,
378 rounding: Some(ButtonLikeRounding::All),
379 tooltip: None,
380 children: SmallVec::new(),
381 cursor_style: CursorStyle::PointingHand,
382 on_click: None,
383 on_right_click: None,
384 layer: None,
385 }
386 }
387
388 pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
389 Self::new(id).rounding(ButtonLikeRounding::Left)
390 }
391
392 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
393 Self::new(id).rounding(ButtonLikeRounding::Right)
394 }
395
396 pub fn opacity(mut self, opacity: f32) -> Self {
397 self.base = self.base.opacity(opacity);
398 self
399 }
400
401 pub fn height(mut self, height: DefiniteLength) -> Self {
402 self.height = Some(height);
403 self
404 }
405
406 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
407 self.rounding = rounding.into();
408 self
409 }
410
411 pub fn on_right_click(
412 mut self,
413 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
414 ) -> Self {
415 self.on_right_click = Some(Box::new(handler));
416 self
417 }
418}
419
420impl Disableable for ButtonLike {
421 fn disabled(mut self, disabled: bool) -> Self {
422 self.disabled = disabled;
423 self
424 }
425}
426
427impl Toggleable for ButtonLike {
428 fn toggle_state(mut self, selected: bool) -> Self {
429 self.selected = selected;
430 self
431 }
432}
433
434impl SelectableButton for ButtonLike {
435 fn selected_style(mut self, style: ButtonStyle) -> Self {
436 self.selected_style = Some(style);
437 self
438 }
439}
440
441impl Clickable for ButtonLike {
442 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
443 self.on_click = Some(Box::new(handler));
444 self
445 }
446
447 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
448 self.cursor_style = cursor_style;
449 self
450 }
451}
452
453impl FixedWidth for ButtonLike {
454 fn width(mut self, width: DefiniteLength) -> Self {
455 self.width = Some(width);
456 self
457 }
458
459 fn full_width(mut self) -> Self {
460 self.width = Some(relative(1.));
461 self
462 }
463}
464
465impl ButtonCommon for ButtonLike {
466 fn id(&self) -> &ElementId {
467 &self.id
468 }
469
470 fn style(mut self, style: ButtonStyle) -> Self {
471 self.style = style;
472 self
473 }
474
475 fn size(mut self, size: ButtonSize) -> Self {
476 self.size = size;
477 self
478 }
479
480 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
481 self.tooltip = Some(Box::new(tooltip));
482 self
483 }
484
485 fn layer(mut self, elevation: ElevationIndex) -> Self {
486 self.layer = Some(elevation);
487 self
488 }
489}
490
491impl VisibleOnHover for ButtonLike {
492 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
493 self.base = self.base.visible_on_hover(group_name);
494 self
495 }
496}
497
498impl ParentElement for ButtonLike {
499 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
500 self.children.extend(elements)
501 }
502}
503
504impl RenderOnce for ButtonLike {
505 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
506 let style = self
507 .selected_style
508 .filter(|_| self.selected)
509 .unwrap_or(self.style);
510
511 self.base
512 .h_flex()
513 .id(self.id.clone())
514 .font_ui(cx)
515 .group("")
516 .flex_none()
517 .h(self.height.unwrap_or(self.size.rems().into()))
518 .when_some(self.width, |this, width| {
519 this.w(width).justify_center().text_center()
520 })
521 .when_some(self.rounding, |this, rounding| match rounding {
522 ButtonLikeRounding::All => this.rounded_sm(),
523 ButtonLikeRounding::Left => this.rounded_l_sm(),
524 ButtonLikeRounding::Right => this.rounded_r_sm(),
525 })
526 .gap(DynamicSpacing::Base04.rems(cx))
527 .map(|this| match self.size {
528 ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
529 ButtonSize::Default | ButtonSize::Compact => {
530 this.px(DynamicSpacing::Base04.rems(cx))
531 }
532 ButtonSize::None => this,
533 })
534 .bg(style.enabled(self.layer, cx).background)
535 .when(self.disabled, |this| this.cursor_not_allowed())
536 .when(!self.disabled, |this| {
537 this.cursor_pointer()
538 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
539 .active(|active| active.bg(style.active(cx).background))
540 })
541 .when_some(
542 self.on_right_click.filter(|_| !self.disabled),
543 |this, on_right_click| {
544 this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
545 window.prevent_default();
546 cx.stop_propagation();
547 })
548 .on_mouse_up(
549 MouseButton::Right,
550 move |event, window, cx| {
551 cx.stop_propagation();
552 let click_event = ClickEvent {
553 down: MouseDownEvent {
554 button: MouseButton::Right,
555 position: event.position,
556 modifiers: event.modifiers,
557 click_count: 1,
558 first_mouse: false,
559 },
560 up: MouseUpEvent {
561 button: MouseButton::Right,
562 position: event.position,
563 modifiers: event.modifiers,
564 click_count: 1,
565 },
566 };
567 (on_right_click)(&click_event, window, cx)
568 },
569 )
570 },
571 )
572 .when_some(
573 self.on_click.filter(|_| !self.disabled),
574 |this, on_click| {
575 this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
576 .on_click(move |event, window, cx| {
577 cx.stop_propagation();
578 (on_click)(event, window, cx)
579 })
580 },
581 )
582 .when_some(self.tooltip, |this, tooltip| {
583 this.tooltip(move |window, cx| tooltip(window, cx))
584 })
585 .children(self.children)
586 }
587}