1use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
2use smallvec::SmallVec;
3
4use crate::h_stack;
5use crate::prelude::*;
6
7pub trait ButtonCommon: Clickable + Disableable {
8 /// A unique element id to help identify the button.
9 fn id(&self) -> &ElementId;
10 /// The visual style of the button.
11 ///
12 /// Mosty commonly will be `ButtonStyle::Subtle`, or `ButtonStyle::Filled`
13 /// for an emphasized button.
14 fn style(self, style: ButtonStyle) -> Self;
15 /// The size of the button.
16 ///
17 /// Most buttons will use the default size.
18 ///
19 /// ButtonSize can also be used to help build non-button elements
20 /// that are consistently sized with buttons.
21 fn size(self, size: ButtonSize) -> Self;
22 /// The tooltip that shows when a user hovers over the button.
23 ///
24 /// Nearly all interactable elements should have a tooltip. Some example
25 /// exceptions might a scroll bar, or a slider.
26 fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
27}
28
29#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
30pub enum ButtonStyle {
31 /// A filled button with a solid background color. Provides emphasis versus
32 /// the more common subtle button.
33 Filled,
34 /// 🚧 Under construction 🚧
35 ///
36 /// Used to emphasize a button in some way, like a selected state, or a semantic
37 /// coloring like an error or success button.
38 Tinted,
39 /// The default button style, used for most buttons. Has a transparent background,
40 /// but has a background color to indicate states like hover and active.
41 #[default]
42 Subtle,
43 /// Used for buttons that only change forground color on hover and active states.
44 ///
45 /// TODO: Better docs for this.
46 Transparent,
47}
48
49#[derive(Debug, Clone)]
50pub(crate) struct ButtonLikeStyles {
51 pub background: Hsla,
52 #[allow(unused)]
53 pub border_color: Hsla,
54 #[allow(unused)]
55 pub label_color: Hsla,
56 #[allow(unused)]
57 pub icon_color: Hsla,
58}
59
60impl ButtonStyle {
61 pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
62 match self {
63 ButtonStyle::Filled => ButtonLikeStyles {
64 background: cx.theme().colors().element_background,
65 border_color: transparent_black(),
66 label_color: Color::Default.color(cx),
67 icon_color: Color::Default.color(cx),
68 },
69 ButtonStyle::Tinted => ButtonLikeStyles {
70 background: gpui::red(),
71 border_color: gpui::red(),
72 label_color: gpui::red(),
73 icon_color: gpui::red(),
74 },
75 ButtonStyle::Subtle => ButtonLikeStyles {
76 background: cx.theme().colors().ghost_element_background,
77 border_color: transparent_black(),
78 label_color: Color::Default.color(cx),
79 icon_color: Color::Default.color(cx),
80 },
81 ButtonStyle::Transparent => ButtonLikeStyles {
82 background: transparent_black(),
83 border_color: transparent_black(),
84 label_color: Color::Default.color(cx),
85 icon_color: Color::Default.color(cx),
86 },
87 }
88 }
89
90 pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
91 match self {
92 ButtonStyle::Filled => ButtonLikeStyles {
93 background: cx.theme().colors().element_hover,
94 border_color: transparent_black(),
95 label_color: Color::Default.color(cx),
96 icon_color: Color::Default.color(cx),
97 },
98 ButtonStyle::Tinted => ButtonLikeStyles {
99 background: gpui::red(),
100 border_color: gpui::red(),
101 label_color: gpui::red(),
102 icon_color: gpui::red(),
103 },
104 ButtonStyle::Subtle => ButtonLikeStyles {
105 background: cx.theme().colors().ghost_element_hover,
106 border_color: transparent_black(),
107 label_color: Color::Default.color(cx),
108 icon_color: Color::Default.color(cx),
109 },
110 ButtonStyle::Transparent => ButtonLikeStyles {
111 background: transparent_black(),
112 border_color: transparent_black(),
113 // TODO: These are not great
114 label_color: Color::Muted.color(cx),
115 // TODO: These are not great
116 icon_color: Color::Muted.color(cx),
117 },
118 }
119 }
120
121 pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
122 match self {
123 ButtonStyle::Filled => ButtonLikeStyles {
124 background: cx.theme().colors().element_active,
125 border_color: transparent_black(),
126 label_color: Color::Default.color(cx),
127 icon_color: Color::Default.color(cx),
128 },
129 ButtonStyle::Tinted => ButtonLikeStyles {
130 background: gpui::red(),
131 border_color: gpui::red(),
132 label_color: gpui::red(),
133 icon_color: gpui::red(),
134 },
135 ButtonStyle::Subtle => ButtonLikeStyles {
136 background: cx.theme().colors().ghost_element_active,
137 border_color: transparent_black(),
138 label_color: Color::Default.color(cx),
139 icon_color: Color::Default.color(cx),
140 },
141 ButtonStyle::Transparent => ButtonLikeStyles {
142 background: transparent_black(),
143 border_color: transparent_black(),
144 // TODO: These are not great
145 label_color: Color::Muted.color(cx),
146 // TODO: These are not great
147 icon_color: Color::Muted.color(cx),
148 },
149 }
150 }
151
152 #[allow(unused)]
153 pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
154 match self {
155 ButtonStyle::Filled => ButtonLikeStyles {
156 background: cx.theme().colors().element_background,
157 border_color: cx.theme().colors().border_focused,
158 label_color: Color::Default.color(cx),
159 icon_color: Color::Default.color(cx),
160 },
161 ButtonStyle::Tinted => ButtonLikeStyles {
162 background: gpui::red(),
163 border_color: gpui::red(),
164 label_color: gpui::red(),
165 icon_color: gpui::red(),
166 },
167 ButtonStyle::Subtle => ButtonLikeStyles {
168 background: cx.theme().colors().ghost_element_background,
169 border_color: cx.theme().colors().border_focused,
170 label_color: Color::Default.color(cx),
171 icon_color: Color::Default.color(cx),
172 },
173 ButtonStyle::Transparent => ButtonLikeStyles {
174 background: transparent_black(),
175 border_color: cx.theme().colors().border_focused,
176 label_color: Color::Accent.color(cx),
177 icon_color: Color::Accent.color(cx),
178 },
179 }
180 }
181
182 pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
183 match self {
184 ButtonStyle::Filled => ButtonLikeStyles {
185 background: cx.theme().colors().element_disabled,
186 border_color: cx.theme().colors().border_disabled,
187 label_color: Color::Disabled.color(cx),
188 icon_color: Color::Disabled.color(cx),
189 },
190 ButtonStyle::Tinted => ButtonLikeStyles {
191 background: gpui::red(),
192 border_color: gpui::red(),
193 label_color: gpui::red(),
194 icon_color: gpui::red(),
195 },
196 ButtonStyle::Subtle => ButtonLikeStyles {
197 background: cx.theme().colors().ghost_element_disabled,
198 border_color: cx.theme().colors().border_disabled,
199 label_color: Color::Disabled.color(cx),
200 icon_color: Color::Disabled.color(cx),
201 },
202 ButtonStyle::Transparent => ButtonLikeStyles {
203 background: transparent_black(),
204 border_color: transparent_black(),
205 label_color: Color::Disabled.color(cx),
206 icon_color: Color::Disabled.color(cx),
207 },
208 }
209 }
210}
211
212/// ButtonSize can also be used to help build non-button elements
213/// that are consistently sized with buttons.
214#[derive(Default, PartialEq, Clone, Copy)]
215pub enum ButtonSize {
216 #[default]
217 Default,
218 Compact,
219 None,
220}
221
222impl ButtonSize {
223 fn height(self) -> Rems {
224 match self {
225 ButtonSize::Default => rems(22. / 16.),
226 ButtonSize::Compact => rems(18. / 16.),
227 ButtonSize::None => rems(16. / 16.),
228 }
229 }
230}
231
232/// A button-like element that can be used to create a custom button when
233/// prebuilt buttons are not sufficient. Use this sparingly, as it is
234/// unconstrained and may make the UI feel less consistent.
235///
236/// This is also used to build the prebuilt buttons.
237#[derive(IntoElement)]
238pub struct ButtonLike {
239 id: ElementId,
240 pub(super) style: ButtonStyle,
241 pub(super) disabled: bool,
242 pub(super) selected: bool,
243 size: ButtonSize,
244 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
245 on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
246 children: SmallVec<[AnyElement; 2]>,
247}
248
249impl ButtonLike {
250 pub fn new(id: impl Into<ElementId>) -> Self {
251 Self {
252 id: id.into(),
253 style: ButtonStyle::default(),
254 disabled: false,
255 selected: false,
256 size: ButtonSize::Default,
257 tooltip: None,
258 children: SmallVec::new(),
259 on_click: None,
260 }
261 }
262}
263
264impl Disableable for ButtonLike {
265 fn disabled(mut self, disabled: bool) -> Self {
266 self.disabled = disabled;
267 self
268 }
269}
270
271impl Selectable for ButtonLike {
272 fn selected(mut self, selected: bool) -> Self {
273 self.selected = selected;
274 self
275 }
276}
277
278impl Clickable for ButtonLike {
279 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
280 self.on_click = Some(Box::new(handler));
281 self
282 }
283}
284
285impl ButtonCommon for ButtonLike {
286 fn id(&self) -> &ElementId {
287 &self.id
288 }
289
290 fn style(mut self, style: ButtonStyle) -> Self {
291 self.style = style;
292 self
293 }
294
295 fn size(mut self, size: ButtonSize) -> Self {
296 self.size = size;
297 self
298 }
299
300 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
301 self.tooltip = Some(Box::new(tooltip));
302 self
303 }
304}
305
306impl ParentElement for ButtonLike {
307 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
308 &mut self.children
309 }
310}
311
312impl RenderOnce for ButtonLike {
313 type Rendered = Stateful<Div>;
314
315 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
316 h_stack()
317 .id(self.id.clone())
318 .h(self.size.height())
319 .rounded_md()
320 .when(!self.disabled, |el| el.cursor_pointer())
321 .gap_1()
322 .px_1()
323 .bg(self.style.enabled(cx).background)
324 .hover(|hover| hover.bg(self.style.hovered(cx).background))
325 .active(|active| active.bg(self.style.active(cx).background))
326 .when_some(
327 self.on_click.filter(|_| !self.disabled),
328 |this, on_click| {
329 this.on_click(move |event, cx| {
330 cx.stop_propagation();
331 (on_click)(event, cx)
332 })
333 },
334 )
335 .when_some(self.tooltip, |this, tooltip| {
336 if !self.selected {
337 this.tooltip(move |cx| tooltip(cx))
338 } else {
339 this
340 }
341 })
342 .children(self.children)
343 }
344}