1use gpui::{relative, DefiniteLength, MouseButton};
2use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
3use smallvec::SmallVec;
4
5use crate::prelude::*;
6
7/// A trait for buttons that can be Selected. Enables setting the [ButtonStyle] of a button when it is selected.
8pub trait SelectableButton: Selectable {
9 fn selected_style(self, style: ButtonStyle) -> Self;
10}
11
12/// A common set of traits all buttons must implement.
13pub trait ButtonCommon: Clickable + Disableable {
14 /// A unique element ID to identify the button.
15 fn id(&self) -> &ElementId;
16
17 /// The visual style of the button.
18 ///
19 /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
20 /// for an emphasized button.
21 fn style(self, style: ButtonStyle) -> Self;
22
23 /// The size of the button.
24 ///
25 /// Most buttons will use the default size.
26 ///
27 /// [`ButtonSize`] can also be used to help build non-button elements
28 /// that are consistently sized with buttons.
29 fn size(self, size: ButtonSize) -> Self;
30
31 /// The tooltip that shows when a user hovers over the button.
32 ///
33 /// Nearly all interactable elements should have a tooltip. Some example
34 /// exceptions might a scroll bar, or a slider.
35 fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
36}
37
38#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
39pub enum IconPosition {
40 #[default]
41 Start,
42 End,
43}
44
45#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
46pub enum TintColor {
47 #[default]
48 Accent,
49 Negative,
50 Warning,
51}
52
53impl TintColor {
54 fn button_like_style(self, cx: &mut WindowContext) -> ButtonLikeStyles {
55 match self {
56 TintColor::Accent => ButtonLikeStyles {
57 background: cx.theme().status().info_background,
58 border_color: cx.theme().status().info_border,
59 label_color: cx.theme().colors().text,
60 icon_color: cx.theme().colors().text,
61 },
62 TintColor::Negative => ButtonLikeStyles {
63 background: cx.theme().status().error_background,
64 border_color: cx.theme().status().error_border,
65 label_color: cx.theme().colors().text,
66 icon_color: cx.theme().colors().text,
67 },
68 TintColor::Warning => ButtonLikeStyles {
69 background: cx.theme().status().warning_background,
70 border_color: cx.theme().status().warning_border,
71 label_color: cx.theme().colors().text,
72 icon_color: cx.theme().colors().text,
73 },
74 }
75 }
76}
77
78impl From<TintColor> for Color {
79 fn from(tint: TintColor) -> Self {
80 match tint {
81 TintColor::Accent => Color::Accent,
82 TintColor::Negative => Color::Error,
83 TintColor::Warning => Color::Warning,
84 }
85 }
86}
87
88// Used to go from ButtonStyle -> Color through tint colors.
89impl From<ButtonStyle> for Color {
90 fn from(style: ButtonStyle) -> Self {
91 match style {
92 ButtonStyle::Tinted(tint) => tint.into(),
93 _ => Color::Default,
94 }
95 }
96}
97
98/// Sets the visual appearance of a button.
99#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
100pub enum ButtonStyle {
101 /// A filled button with a solid background color. Provides emphasis versus
102 /// the more common subtle button.
103 Filled,
104
105 /// Used to emphasize a button in some way, like a selected state, or a semantic
106 /// coloring like an error or success button.
107 Tinted(TintColor),
108
109 /// The default button style, used for most buttons. Has a transparent background,
110 /// but has a background color to indicate states like hover and active.
111 #[default]
112 Subtle,
113
114 /// Used for buttons that only change forground color on hover and active states.
115 ///
116 /// TODO: Better docs for this.
117 Transparent,
118}
119
120#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
121pub(crate) enum ButtonLikeRounding {
122 All,
123 Left,
124 Right,
125}
126
127#[derive(Debug, Clone)]
128pub(crate) struct ButtonLikeStyles {
129 pub background: Hsla,
130 #[allow(unused)]
131 pub border_color: Hsla,
132 #[allow(unused)]
133 pub label_color: Hsla,
134 #[allow(unused)]
135 pub icon_color: Hsla,
136}
137
138impl ButtonStyle {
139 pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
140 match self {
141 ButtonStyle::Filled => ButtonLikeStyles {
142 background: cx.theme().colors().element_background,
143 border_color: transparent_black(),
144 label_color: Color::Default.color(cx),
145 icon_color: Color::Default.color(cx),
146 },
147 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
148 ButtonStyle::Subtle => ButtonLikeStyles {
149 background: cx.theme().colors().ghost_element_background,
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 label_color: Color::Default.color(cx),
158 icon_color: Color::Default.color(cx),
159 },
160 }
161 }
162
163 pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
164 match self {
165 ButtonStyle::Filled => ButtonLikeStyles {
166 background: cx.theme().colors().element_hover,
167 border_color: transparent_black(),
168 label_color: Color::Default.color(cx),
169 icon_color: Color::Default.color(cx),
170 },
171 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
172 ButtonStyle::Subtle => ButtonLikeStyles {
173 background: cx.theme().colors().ghost_element_hover,
174 border_color: transparent_black(),
175 label_color: Color::Default.color(cx),
176 icon_color: Color::Default.color(cx),
177 },
178 ButtonStyle::Transparent => ButtonLikeStyles {
179 background: transparent_black(),
180 border_color: transparent_black(),
181 // TODO: These are not great
182 label_color: Color::Muted.color(cx),
183 // TODO: These are not great
184 icon_color: Color::Muted.color(cx),
185 },
186 }
187 }
188
189 pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
190 match self {
191 ButtonStyle::Filled => ButtonLikeStyles {
192 background: cx.theme().colors().element_active,
193 border_color: transparent_black(),
194 label_color: Color::Default.color(cx),
195 icon_color: Color::Default.color(cx),
196 },
197 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
198 ButtonStyle::Subtle => ButtonLikeStyles {
199 background: cx.theme().colors().ghost_element_active,
200 border_color: transparent_black(),
201 label_color: Color::Default.color(cx),
202 icon_color: Color::Default.color(cx),
203 },
204 ButtonStyle::Transparent => ButtonLikeStyles {
205 background: transparent_black(),
206 border_color: transparent_black(),
207 // TODO: These are not great
208 label_color: Color::Muted.color(cx),
209 // TODO: These are not great
210 icon_color: Color::Muted.color(cx),
211 },
212 }
213 }
214
215 #[allow(unused)]
216 pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
217 match self {
218 ButtonStyle::Filled => ButtonLikeStyles {
219 background: cx.theme().colors().element_background,
220 border_color: cx.theme().colors().border_focused,
221 label_color: Color::Default.color(cx),
222 icon_color: Color::Default.color(cx),
223 },
224 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
225 ButtonStyle::Subtle => ButtonLikeStyles {
226 background: cx.theme().colors().ghost_element_background,
227 border_color: cx.theme().colors().border_focused,
228 label_color: Color::Default.color(cx),
229 icon_color: Color::Default.color(cx),
230 },
231 ButtonStyle::Transparent => ButtonLikeStyles {
232 background: transparent_black(),
233 border_color: cx.theme().colors().border_focused,
234 label_color: Color::Accent.color(cx),
235 icon_color: Color::Accent.color(cx),
236 },
237 }
238 }
239
240 #[allow(unused)]
241 pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
242 match self {
243 ButtonStyle::Filled => ButtonLikeStyles {
244 background: cx.theme().colors().element_disabled,
245 border_color: cx.theme().colors().border_disabled,
246 label_color: Color::Disabled.color(cx),
247 icon_color: Color::Disabled.color(cx),
248 },
249 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
250 ButtonStyle::Subtle => ButtonLikeStyles {
251 background: cx.theme().colors().ghost_element_disabled,
252 border_color: cx.theme().colors().border_disabled,
253 label_color: Color::Disabled.color(cx),
254 icon_color: Color::Disabled.color(cx),
255 },
256 ButtonStyle::Transparent => ButtonLikeStyles {
257 background: transparent_black(),
258 border_color: transparent_black(),
259 label_color: Color::Disabled.color(cx),
260 icon_color: Color::Disabled.color(cx),
261 },
262 }
263 }
264}
265
266/// Sets the height of a button. Can also be used to size non-button elements to align with [Button]s.
267#[derive(Default, PartialEq, Clone, Copy)]
268pub enum ButtonSize {
269 Large,
270 #[default]
271 Default,
272 Compact,
273 None,
274}
275
276impl ButtonSize {
277 fn height(self) -> Rems {
278 match self {
279 ButtonSize::Large => rems(32. / 16.),
280 ButtonSize::Default => rems(22. / 16.),
281 ButtonSize::Compact => rems(18. / 16.),
282 ButtonSize::None => rems(16. / 16.),
283 }
284 }
285}
286
287/// A button-like element that can be used to create a custom button when
288/// prebuilt buttons are not sufficient. Use this sparingly, as it is
289/// unconstrained and may make the UI feel less consistent.
290///
291/// This is also used to build the prebuilt buttons.
292#[derive(IntoElement)]
293pub struct ButtonLike {
294 base: Div,
295 id: ElementId,
296 pub(super) style: ButtonStyle,
297 pub(super) disabled: bool,
298 pub(super) selected: bool,
299 pub(super) selected_style: Option<ButtonStyle>,
300 pub(super) width: Option<DefiniteLength>,
301 size: ButtonSize,
302 rounding: Option<ButtonLikeRounding>,
303 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
304 on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
305 children: SmallVec<[AnyElement; 2]>,
306}
307
308impl ButtonLike {
309 pub fn new(id: impl Into<ElementId>) -> Self {
310 Self {
311 base: div(),
312 id: id.into(),
313 style: ButtonStyle::default(),
314 disabled: false,
315 selected: false,
316 selected_style: None,
317 width: None,
318 size: ButtonSize::Default,
319 rounding: Some(ButtonLikeRounding::All),
320 tooltip: None,
321 children: SmallVec::new(),
322 on_click: None,
323 }
324 }
325
326 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
327 self.rounding = rounding.into();
328 self
329 }
330}
331
332impl Disableable for ButtonLike {
333 fn disabled(mut self, disabled: bool) -> Self {
334 self.disabled = disabled;
335 self
336 }
337}
338
339impl Selectable for ButtonLike {
340 fn selected(mut self, selected: bool) -> Self {
341 self.selected = selected;
342 self
343 }
344}
345
346impl SelectableButton for ButtonLike {
347 fn selected_style(mut self, style: ButtonStyle) -> Self {
348 self.selected_style = Some(style);
349 self
350 }
351}
352
353impl Clickable for ButtonLike {
354 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
355 self.on_click = Some(Box::new(handler));
356 self
357 }
358}
359
360impl FixedWidth for ButtonLike {
361 fn width(mut self, width: DefiniteLength) -> Self {
362 self.width = Some(width);
363 self
364 }
365
366 fn full_width(mut self) -> Self {
367 self.width = Some(relative(1.));
368 self
369 }
370}
371
372impl ButtonCommon for ButtonLike {
373 fn id(&self) -> &ElementId {
374 &self.id
375 }
376
377 fn style(mut self, style: ButtonStyle) -> Self {
378 self.style = style;
379 self
380 }
381
382 fn size(mut self, size: ButtonSize) -> Self {
383 self.size = size;
384 self
385 }
386
387 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
388 self.tooltip = Some(Box::new(tooltip));
389 self
390 }
391}
392
393impl VisibleOnHover for ButtonLike {
394 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
395 self.base = self.base.visible_on_hover(group_name);
396 self
397 }
398}
399
400impl ParentElement for ButtonLike {
401 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
402 &mut self.children
403 }
404}
405
406impl RenderOnce for ButtonLike {
407 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
408 let style = self
409 .selected_style
410 .filter(|_| self.selected)
411 .unwrap_or(self.style);
412
413 self.base
414 .h_flex()
415 .id(self.id.clone())
416 .group("")
417 .flex_none()
418 .h(self.size.height())
419 .when_some(self.width, |this, width| this.w(width).justify_center())
420 .when_some(self.rounding, |this, rounding| match rounding {
421 ButtonLikeRounding::All => this.rounded_md(),
422 ButtonLikeRounding::Left => this.rounded_l_md(),
423 ButtonLikeRounding::Right => this.rounded_r_md(),
424 })
425 .gap_1()
426 .map(|this| match self.size {
427 ButtonSize::Large => this.px_2(),
428 ButtonSize::Default | ButtonSize::Compact => this.px_1(),
429 ButtonSize::None => this,
430 })
431 .bg(style.enabled(cx).background)
432 .when(self.disabled, |this| this.cursor_not_allowed())
433 .when(!self.disabled, |this| {
434 this.cursor_pointer()
435 .hover(|hover| hover.bg(style.hovered(cx).background))
436 .active(|active| active.bg(style.active(cx).background))
437 })
438 .when_some(
439 self.on_click.filter(|_| !self.disabled),
440 |this, on_click| {
441 this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
442 .on_click(move |event, cx| {
443 cx.stop_propagation();
444 (on_click)(event, cx)
445 })
446 },
447 )
448 .when_some(self.tooltip, |this, tooltip| {
449 this.tooltip(move |cx| tooltip(cx))
450 })
451 .children(self.children)
452 }
453}