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