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