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