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 /// Usually used as a secondary action that should have more emphasis than
130 /// a fully transparent button.
131 Outlined,
132
133 /// The default button style, used for most buttons. Has a transparent background,
134 /// but has a background color to indicate states like hover and active.
135 #[default]
136 Subtle,
137
138 /// Used for buttons that only change foreground color on hover and active states.
139 ///
140 /// TODO: Better docs for this.
141 Transparent,
142}
143
144#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
145pub(crate) enum ButtonLikeRounding {
146 All,
147 Left,
148 Right,
149}
150
151#[derive(Debug, Clone)]
152pub(crate) struct ButtonLikeStyles {
153 pub background: Hsla,
154 #[allow(unused)]
155 pub border_color: Hsla,
156 #[allow(unused)]
157 pub label_color: Hsla,
158 #[allow(unused)]
159 pub icon_color: Hsla,
160}
161
162fn element_bg_from_elevation(elevation: Option<ElevationIndex>, cx: &mut App) -> Hsla {
163 match elevation {
164 Some(ElevationIndex::Background) => cx.theme().colors().element_background,
165 Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
166 Some(ElevationIndex::Surface) => cx.theme().colors().surface_background,
167 Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
168 _ => cx.theme().colors().element_background,
169 }
170}
171
172impl ButtonStyle {
173 pub(crate) fn enabled(
174 self,
175 elevation: Option<ElevationIndex>,
176
177 cx: &mut App,
178 ) -> ButtonLikeStyles {
179 match self {
180 ButtonStyle::Filled => ButtonLikeStyles {
181 background: element_bg_from_elevation(elevation, cx),
182 border_color: transparent_black(),
183 label_color: Color::Default.color(cx),
184 icon_color: Color::Default.color(cx),
185 },
186 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
187 ButtonStyle::Outlined => ButtonLikeStyles {
188 background: element_bg_from_elevation(elevation, cx),
189 border_color: cx.theme().colors().border_variant,
190 label_color: Color::Default.color(cx),
191 icon_color: Color::Default.color(cx),
192 },
193 ButtonStyle::Subtle => ButtonLikeStyles {
194 background: cx.theme().colors().ghost_element_background,
195 border_color: transparent_black(),
196 label_color: Color::Default.color(cx),
197 icon_color: Color::Default.color(cx),
198 },
199 ButtonStyle::Transparent => ButtonLikeStyles {
200 background: transparent_black(),
201 border_color: transparent_black(),
202 label_color: Color::Default.color(cx),
203 icon_color: Color::Default.color(cx),
204 },
205 }
206 }
207
208 pub(crate) fn hovered(
209 self,
210 elevation: Option<ElevationIndex>,
211
212 cx: &mut App,
213 ) -> ButtonLikeStyles {
214 match self {
215 ButtonStyle::Filled => {
216 let mut filled_background = element_bg_from_elevation(elevation, cx);
217 filled_background.fade_out(0.92);
218
219 ButtonLikeStyles {
220 background: filled_background,
221 border_color: transparent_black(),
222 label_color: Color::Default.color(cx),
223 icon_color: Color::Default.color(cx),
224 }
225 }
226 ButtonStyle::Tinted(tint) => {
227 let mut styles = tint.button_like_style(cx);
228 let theme = cx.theme();
229 styles.background = theme.darken(styles.background, 0.05, 0.2);
230 styles
231 }
232 ButtonStyle::Outlined => ButtonLikeStyles {
233 background: cx.theme().colors().ghost_element_hover,
234 border_color: cx.theme().colors().border,
235 label_color: Color::Default.color(cx),
236 icon_color: Color::Default.color(cx),
237 },
238 ButtonStyle::Subtle => ButtonLikeStyles {
239 background: cx.theme().colors().ghost_element_hover,
240 border_color: transparent_black(),
241 label_color: Color::Default.color(cx),
242 icon_color: Color::Default.color(cx),
243 },
244 ButtonStyle::Transparent => ButtonLikeStyles {
245 background: transparent_black(),
246 border_color: transparent_black(),
247 // TODO: These are not great
248 label_color: Color::Muted.color(cx),
249 // TODO: These are not great
250 icon_color: Color::Muted.color(cx),
251 },
252 }
253 }
254
255 pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
256 match self {
257 ButtonStyle::Filled => ButtonLikeStyles {
258 background: cx.theme().colors().element_active,
259 border_color: transparent_black(),
260 label_color: Color::Default.color(cx),
261 icon_color: Color::Default.color(cx),
262 },
263 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
264 ButtonStyle::Subtle => ButtonLikeStyles {
265 background: cx.theme().colors().ghost_element_active,
266 border_color: transparent_black(),
267 label_color: Color::Default.color(cx),
268 icon_color: Color::Default.color(cx),
269 },
270 ButtonStyle::Outlined => ButtonLikeStyles {
271 background: cx.theme().colors().element_active,
272 border_color: cx.theme().colors().border_variant,
273 label_color: Color::Default.color(cx),
274 icon_color: Color::Default.color(cx),
275 },
276 ButtonStyle::Transparent => ButtonLikeStyles {
277 background: transparent_black(),
278 border_color: transparent_black(),
279 // TODO: These are not great
280 label_color: Color::Muted.color(cx),
281 // TODO: These are not great
282 icon_color: Color::Muted.color(cx),
283 },
284 }
285 }
286
287 #[allow(unused)]
288 pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
289 match self {
290 ButtonStyle::Filled => ButtonLikeStyles {
291 background: cx.theme().colors().element_background,
292 border_color: cx.theme().colors().border_focused,
293 label_color: Color::Default.color(cx),
294 icon_color: Color::Default.color(cx),
295 },
296 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
297 ButtonStyle::Subtle => ButtonLikeStyles {
298 background: cx.theme().colors().ghost_element_background,
299 border_color: cx.theme().colors().border_focused,
300 label_color: Color::Default.color(cx),
301 icon_color: Color::Default.color(cx),
302 },
303 ButtonStyle::Outlined => ButtonLikeStyles {
304 background: cx.theme().colors().ghost_element_background,
305 border_color: cx.theme().colors().border,
306 label_color: Color::Default.color(cx),
307 icon_color: Color::Default.color(cx),
308 },
309 ButtonStyle::Transparent => ButtonLikeStyles {
310 background: transparent_black(),
311 border_color: cx.theme().colors().border_focused,
312 label_color: Color::Accent.color(cx),
313 icon_color: Color::Accent.color(cx),
314 },
315 }
316 }
317
318 #[allow(unused)]
319 pub(crate) fn disabled(
320 self,
321 elevation: Option<ElevationIndex>,
322 window: &mut Window,
323 cx: &mut App,
324 ) -> ButtonLikeStyles {
325 match self {
326 ButtonStyle::Filled => ButtonLikeStyles {
327 background: cx.theme().colors().element_disabled,
328 border_color: cx.theme().colors().border_disabled,
329 label_color: Color::Disabled.color(cx),
330 icon_color: Color::Disabled.color(cx),
331 },
332 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
333 ButtonStyle::Subtle => ButtonLikeStyles {
334 background: cx.theme().colors().ghost_element_disabled,
335 border_color: cx.theme().colors().border_disabled,
336 label_color: Color::Disabled.color(cx),
337 icon_color: Color::Disabled.color(cx),
338 },
339 ButtonStyle::Outlined => ButtonLikeStyles {
340 background: cx.theme().colors().element_disabled,
341 border_color: cx.theme().colors().border_disabled,
342 label_color: Color::Default.color(cx),
343 icon_color: Color::Default.color(cx),
344 },
345 ButtonStyle::Transparent => ButtonLikeStyles {
346 background: transparent_black(),
347 border_color: transparent_black(),
348 label_color: Color::Disabled.color(cx),
349 icon_color: Color::Disabled.color(cx),
350 },
351 }
352 }
353}
354
355/// The height of a button.
356///
357/// Can also be used to size non-button elements to align with [`Button`]s.
358#[derive(Default, PartialEq, Clone, Copy)]
359pub enum ButtonSize {
360 Large,
361 #[default]
362 Default,
363 Compact,
364 None,
365}
366
367impl ButtonSize {
368 pub fn rems(self) -> Rems {
369 match self {
370 ButtonSize::Large => rems_from_px(32.),
371 ButtonSize::Default => rems_from_px(22.),
372 ButtonSize::Compact => rems_from_px(18.),
373 ButtonSize::None => rems_from_px(16.),
374 }
375 }
376}
377
378/// A button-like element that can be used to create a custom button when
379/// prebuilt buttons are not sufficient. Use this sparingly, as it is
380/// unconstrained and may make the UI feel less consistent.
381///
382/// This is also used to build the prebuilt buttons.
383#[derive(IntoElement, Documented, RegisterComponent)]
384pub struct ButtonLike {
385 pub(super) base: Div,
386 id: ElementId,
387 pub(super) style: ButtonStyle,
388 pub(super) disabled: bool,
389 pub(super) selected: bool,
390 pub(super) selected_style: Option<ButtonStyle>,
391 pub(super) width: Option<DefiniteLength>,
392 pub(super) height: Option<DefiniteLength>,
393 pub(super) layer: Option<ElevationIndex>,
394 size: ButtonSize,
395 rounding: Option<ButtonLikeRounding>,
396 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
397 cursor_style: CursorStyle,
398 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
399 on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
400 children: SmallVec<[AnyElement; 2]>,
401}
402
403impl ButtonLike {
404 pub fn new(id: impl Into<ElementId>) -> Self {
405 Self {
406 base: div(),
407 id: id.into(),
408 style: ButtonStyle::default(),
409 disabled: false,
410 selected: false,
411 selected_style: None,
412 width: None,
413 height: None,
414 size: ButtonSize::Default,
415 rounding: Some(ButtonLikeRounding::All),
416 tooltip: None,
417 children: SmallVec::new(),
418 cursor_style: CursorStyle::PointingHand,
419 on_click: None,
420 on_right_click: None,
421 layer: None,
422 }
423 }
424
425 pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
426 Self::new(id).rounding(ButtonLikeRounding::Left)
427 }
428
429 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
430 Self::new(id).rounding(ButtonLikeRounding::Right)
431 }
432
433 pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
434 Self::new(id).rounding(ButtonLikeRounding::All)
435 }
436
437 pub fn opacity(mut self, opacity: f32) -> Self {
438 self.base = self.base.opacity(opacity);
439 self
440 }
441
442 pub fn height(mut self, height: DefiniteLength) -> Self {
443 self.height = Some(height);
444 self
445 }
446
447 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
448 self.rounding = rounding.into();
449 self
450 }
451
452 pub fn on_right_click(
453 mut self,
454 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
455 ) -> Self {
456 self.on_right_click = Some(Box::new(handler));
457 self
458 }
459}
460
461impl Disableable for ButtonLike {
462 fn disabled(mut self, disabled: bool) -> Self {
463 self.disabled = disabled;
464 self
465 }
466}
467
468impl Toggleable for ButtonLike {
469 fn toggle_state(mut self, selected: bool) -> Self {
470 self.selected = selected;
471 self
472 }
473}
474
475impl SelectableButton for ButtonLike {
476 fn selected_style(mut self, style: ButtonStyle) -> Self {
477 self.selected_style = Some(style);
478 self
479 }
480}
481
482impl Clickable for ButtonLike {
483 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
484 self.on_click = Some(Box::new(handler));
485 self
486 }
487
488 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
489 self.cursor_style = cursor_style;
490 self
491 }
492}
493
494impl FixedWidth for ButtonLike {
495 fn width(mut self, width: DefiniteLength) -> Self {
496 self.width = Some(width);
497 self
498 }
499
500 fn full_width(mut self) -> Self {
501 self.width = Some(relative(1.));
502 self
503 }
504}
505
506impl ButtonCommon for ButtonLike {
507 fn id(&self) -> &ElementId {
508 &self.id
509 }
510
511 fn style(mut self, style: ButtonStyle) -> Self {
512 self.style = style;
513 self
514 }
515
516 fn size(mut self, size: ButtonSize) -> Self {
517 self.size = size;
518 self
519 }
520
521 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
522 self.tooltip = Some(Box::new(tooltip));
523 self
524 }
525
526 fn layer(mut self, elevation: ElevationIndex) -> Self {
527 self.layer = Some(elevation);
528 self
529 }
530}
531
532impl VisibleOnHover for ButtonLike {
533 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
534 self.base = self.base.visible_on_hover(group_name);
535 self
536 }
537}
538
539impl ParentElement for ButtonLike {
540 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
541 self.children.extend(elements)
542 }
543}
544
545impl RenderOnce for ButtonLike {
546 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
547 let style = self
548 .selected_style
549 .filter(|_| self.selected)
550 .unwrap_or(self.style);
551
552 self.base
553 .h_flex()
554 .id(self.id.clone())
555 .font_ui(cx)
556 .group("")
557 .flex_none()
558 .h(self.height.unwrap_or(self.size.rems().into()))
559 .when_some(self.width, |this, width| {
560 this.w(width).justify_center().text_center()
561 })
562 .when(
563 match self.style {
564 ButtonStyle::Outlined => true,
565 _ => false,
566 },
567 |this| this.border_1(),
568 )
569 .when_some(self.rounding, |this, rounding| match rounding {
570 ButtonLikeRounding::All => this.rounded_sm(),
571 ButtonLikeRounding::Left => this.rounded_l_sm(),
572 ButtonLikeRounding::Right => this.rounded_r_sm(),
573 })
574 .gap(DynamicSpacing::Base04.rems(cx))
575 .map(|this| match self.size {
576 ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
577 ButtonSize::Default | ButtonSize::Compact => {
578 this.px(DynamicSpacing::Base04.rems(cx))
579 }
580 ButtonSize::None => this,
581 })
582 .border_color(style.enabled(self.layer, cx).border_color)
583 .bg(style.enabled(self.layer, cx).background)
584 .when(self.disabled, |this| {
585 if self.cursor_style == CursorStyle::PointingHand {
586 this.cursor_not_allowed()
587 } else {
588 this.cursor(self.cursor_style)
589 }
590 })
591 .when(!self.disabled, |this| {
592 this.cursor(self.cursor_style)
593 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
594 .active(|active| active.bg(style.active(cx).background))
595 })
596 .when_some(
597 self.on_right_click.filter(|_| !self.disabled),
598 |this, on_right_click| {
599 this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
600 window.prevent_default();
601 cx.stop_propagation();
602 })
603 .on_mouse_up(
604 MouseButton::Right,
605 move |event, window, cx| {
606 cx.stop_propagation();
607 let click_event = ClickEvent {
608 down: MouseDownEvent {
609 button: MouseButton::Right,
610 position: event.position,
611 modifiers: event.modifiers,
612 click_count: 1,
613 first_mouse: false,
614 },
615 up: MouseUpEvent {
616 button: MouseButton::Right,
617 position: event.position,
618 modifiers: event.modifiers,
619 click_count: 1,
620 },
621 };
622 (on_right_click)(&click_event, window, cx)
623 },
624 )
625 },
626 )
627 .when_some(
628 self.on_click.filter(|_| !self.disabled),
629 |this, on_click| {
630 this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
631 .on_click(move |event, window, cx| {
632 cx.stop_propagation();
633 (on_click)(event, window, cx)
634 })
635 },
636 )
637 .when_some(self.tooltip, |this, tooltip| {
638 this.tooltip(move |window, cx| tooltip(window, cx))
639 })
640 .children(self.children)
641 }
642}
643
644impl Component for ButtonLike {
645 fn scope() -> ComponentScope {
646 ComponentScope::Input
647 }
648
649 fn sort_name() -> &'static str {
650 // ButtonLike should be at the bottom of the button list
651 "ButtonZ"
652 }
653
654 fn description() -> Option<&'static str> {
655 Some(ButtonLike::DOCS)
656 }
657
658 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
659 Some(
660 v_flex()
661 .gap_6()
662 .children(vec![
663 example_group(vec![
664 single_example(
665 "Default",
666 ButtonLike::new("default")
667 .child(Label::new("Default"))
668 .into_any_element(),
669 ),
670 single_example(
671 "Filled",
672 ButtonLike::new("filled")
673 .style(ButtonStyle::Filled)
674 .child(Label::new("Filled"))
675 .into_any_element(),
676 ),
677 single_example(
678 "Subtle",
679 ButtonLike::new("outline")
680 .style(ButtonStyle::Subtle)
681 .child(Label::new("Subtle"))
682 .into_any_element(),
683 ),
684 single_example(
685 "Tinted",
686 ButtonLike::new("tinted_accent_style")
687 .style(ButtonStyle::Tinted(TintColor::Accent))
688 .child(Label::new("Accent"))
689 .into_any_element(),
690 ),
691 single_example(
692 "Transparent",
693 ButtonLike::new("transparent")
694 .style(ButtonStyle::Transparent)
695 .child(Label::new("Transparent"))
696 .into_any_element(),
697 ),
698 ]),
699 example_group_with_title(
700 "Button Group Constructors",
701 vec![
702 single_example(
703 "Left Rounded",
704 ButtonLike::new_rounded_left("left_rounded")
705 .child(Label::new("Left Rounded"))
706 .style(ButtonStyle::Filled)
707 .into_any_element(),
708 ),
709 single_example(
710 "Right Rounded",
711 ButtonLike::new_rounded_right("right_rounded")
712 .child(Label::new("Right Rounded"))
713 .style(ButtonStyle::Filled)
714 .into_any_element(),
715 ),
716 single_example(
717 "Button Group",
718 h_flex()
719 .gap_px()
720 .child(
721 ButtonLike::new_rounded_left("bg_left")
722 .child(Label::new("Left"))
723 .style(ButtonStyle::Filled),
724 )
725 .child(
726 ButtonLike::new_rounded_right("bg_right")
727 .child(Label::new("Right"))
728 .style(ButtonStyle::Filled),
729 )
730 .into_any_element(),
731 ),
732 ],
733 ),
734 ])
735 .into_any_element(),
736 )
737 }
738}