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