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