Skip to main content

guise/
button.rs

1//! `Button` — the flagship interactive control, with the library's variants,
2//! colors, and sizes.
3
4use gpui::prelude::*;
5use gpui::{div, px, App, ClickEvent, ElementId, FontWeight, IntoElement, SharedString, Window};
6
7use crate::devtools::Probed;
8use crate::input::ClickHandler;
9use crate::style::{surface, ColorValue, Variant};
10use crate::theme::{theme, Size};
11
12/// A clickable button.
13#[derive(IntoElement)]
14pub struct Button {
15  id: ElementId,
16  label: SharedString,
17  variant: Variant,
18  color: ColorValue,
19  size: Size,
20  radius: Option<Size>,
21  full_width: bool,
22  disabled: bool,
23  left_section: Option<gpui::AnyElement>,
24  right_section: Option<gpui::AnyElement>,
25  on_click: Option<ClickHandler>,
26}
27
28impl Button {
29  pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
30    Button {
31      id: id.into(),
32      label: label.into(),
33      variant: Variant::Filled,
34      color: ColorValue::default(),
35      size: Size::Sm,
36      radius: None,
37      full_width: false,
38      disabled: false,
39      left_section: None,
40      right_section: None,
41      on_click: None,
42    }
43  }
44
45  pub fn variant(mut self, variant: Variant) -> Self {
46    self.variant = variant;
47    self
48  }
49
50  /// Override the button color. Accepts a palette `ColorName` or any explicit
51  /// color (e.g. `color!(rgba(34, 139, 230, 1))`).
52  pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
53    self.color = color.into();
54    self
55  }
56
57  pub fn size(mut self, size: Size) -> Self {
58    self.size = size;
59    self
60  }
61
62  pub fn radius(mut self, radius: Size) -> Self {
63    self.radius = Some(radius);
64    self
65  }
66
67  pub fn full_width(mut self, full_width: bool) -> Self {
68    self.full_width = full_width;
69    self
70  }
71
72  pub fn disabled(mut self, disabled: bool) -> Self {
73    self.disabled = disabled;
74    self
75  }
76
77  /// Content shown before the label (e.g. an icon).
78  pub fn left_section(mut self, section: impl IntoElement) -> Self {
79    self.left_section = Some(section.into_any_element());
80    self
81  }
82
83  /// Content shown after the label.
84  pub fn right_section(mut self, section: impl IntoElement) -> Self {
85    self.right_section = Some(section.into_any_element());
86    self
87  }
88
89  pub fn on_click(
90    mut self,
91    handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
92  ) -> Self {
93    self.on_click = Some(Box::new(handler));
94    self
95  }
96
97  /// (height, horizontal padding, font size) for the size scale.
98  fn metrics(&self) -> (f32, f32, f32) {
99    match self.size {
100      Size::Xs => (30.0, 14.0, 12.0),
101      Size::Sm => (36.0, 18.0, 14.0),
102      Size::Md => (42.0, 22.0, 16.0),
103      Size::Lg => (50.0, 26.0, 18.0),
104      Size::Xl => (60.0, 32.0, 20.0),
105    }
106  }
107}
108
109impl RenderOnce for Button {
110  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
111    let t = theme(cx);
112    let s = surface(t, self.color, self.variant);
113    let (height, pad_x, font) = self.metrics();
114    let radius = t.radius(self.radius.unwrap_or(t.default_radius));
115    let focus = t.primary().hsla();
116
117    let mut el = div()
118      .id(self.id)
119      .flex()
120      .items_center()
121      .justify_center()
122      .gap(px(8.0))
123      .h(px(height))
124      .px(px(pad_x))
125      .rounded(px(radius))
126      .bg(s.bg)
127      .text_color(s.fg)
128      .text_size(px(font))
129      .font_weight(FontWeight::SEMIBOLD)
130      .tab_index(0)
131      .focus(move |style| style.border_2().border_color(focus))
132      .on_key_down(crate::input::handle_tab);
133
134    if let Some(border) = s.border {
135      el = el.border_1().border_color(border);
136    }
137    if self.full_width {
138      el = el.w_full();
139    }
140    if let Some(left) = self.left_section {
141      el = el.child(left);
142    }
143    el = el.child(self.label);
144    if let Some(right) = self.right_section {
145      el = el.child(right);
146    }
147
148    let element = if self.disabled {
149      el.tab_stop(false).opacity(0.6)
150    } else {
151      let hover_bg = s.bg_hover;
152      el = el.hover(move |st| st.bg(hover_bg));
153      if let Some(handler) = self.on_click {
154        el = el.on_click(handler);
155      }
156      el
157    };
158
159    element
160      .probe("Button")
161      .attr("variant", self.variant.label())
162      .attr("size", self.size.label())
163      .attr_if("disabled", self.disabled)
164  }
165}