1use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
2use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
3use smallvec::SmallVec;
4
5use crate::{prelude::*, Elevation, ElevationIndex, Spacing};
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 fn layer(self, elevation: ElevationIndex) -> Self;
38}
39
40#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
41pub enum IconPosition {
42 #[default]
43 Start,
44 End,
45}
46
47#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
48pub enum TintColor {
49 #[default]
50 Accent,
51 Negative,
52 Warning,
53}
54
55impl TintColor {
56 fn button_like_style(self, cx: &mut WindowContext) -> ButtonLikeStyles {
57 match self {
58 TintColor::Accent => ButtonLikeStyles {
59 background: cx.theme().status().info_background,
60 border_color: cx.theme().status().info_border,
61 label_color: cx.theme().colors().text,
62 icon_color: cx.theme().colors().text,
63 },
64 TintColor::Negative => ButtonLikeStyles {
65 background: cx.theme().status().error_background,
66 border_color: cx.theme().status().error_border,
67 label_color: cx.theme().colors().text,
68 icon_color: cx.theme().colors().text,
69 },
70 TintColor::Warning => ButtonLikeStyles {
71 background: cx.theme().status().warning_background,
72 border_color: cx.theme().status().warning_border,
73 label_color: cx.theme().colors().text,
74 icon_color: cx.theme().colors().text,
75 },
76 }
77 }
78}
79
80impl From<TintColor> for Color {
81 fn from(tint: TintColor) -> Self {
82 match tint {
83 TintColor::Accent => Color::Accent,
84 TintColor::Negative => Color::Error,
85 TintColor::Warning => Color::Warning,
86 }
87 }
88}
89
90// Used to go from ButtonStyle -> Color through tint colors.
91impl From<ButtonStyle> for Color {
92 fn from(style: ButtonStyle) -> Self {
93 match style {
94 ButtonStyle::Tinted(tint) => tint.into(),
95 _ => Color::Default,
96 }
97 }
98}
99
100/// The visual appearance of a button.
101#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
102pub enum ButtonStyle {
103 /// A filled button with a solid background color. Provides emphasis versus
104 /// the more common subtle button.
105 Filled,
106
107 /// Used to emphasize a button in some way, like a selected state, or a semantic
108 /// coloring like an error or success button.
109 Tinted(TintColor),
110
111 /// The default button style, used for most buttons. Has a transparent background,
112 /// but has a background color to indicate states like hover and active.
113 #[default]
114 Subtle,
115
116 /// Used for buttons that only change foreground color on hover and active states.
117 ///
118 /// TODO: Better docs for this.
119 Transparent,
120}
121
122#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
123pub(crate) enum ButtonLikeRounding {
124 All,
125 Left,
126 Right,
127}
128
129#[derive(Debug, Clone)]
130pub(crate) struct ButtonLikeStyles {
131 pub background: Hsla,
132 #[allow(unused)]
133 pub border_color: Hsla,
134 #[allow(unused)]
135 pub label_color: Hsla,
136 #[allow(unused)]
137 pub icon_color: Hsla,
138}
139
140fn element_bg_from_elevation(elevation: Option<Elevation>, cx: &mut WindowContext) -> Hsla {
141 match elevation {
142 Some(Elevation::ElevationIndex(ElevationIndex::Background)) => {
143 cx.theme().colors().element_background
144 }
145 Some(Elevation::ElevationIndex(ElevationIndex::ElevatedSurface)) => {
146 cx.theme().colors().surface_background
147 }
148 Some(Elevation::ElevationIndex(ElevationIndex::Surface)) => {
149 cx.theme().colors().elevated_surface_background
150 }
151 Some(Elevation::ElevationIndex(ElevationIndex::ModalSurface)) => {
152 cx.theme().colors().background
153 }
154 _ => cx.theme().colors().element_background,
155 }
156}
157
158impl ButtonStyle {
159 pub(crate) fn enabled(
160 self,
161 elevation: Option<Elevation>,
162 cx: &mut WindowContext,
163 ) -> ButtonLikeStyles {
164 let filled_background = element_bg_from_elevation(elevation, cx);
165
166 match self {
167 ButtonStyle::Filled => ButtonLikeStyles {
168 background: filled_background,
169 border_color: transparent_black(),
170 label_color: Color::Default.color(cx),
171 icon_color: Color::Default.color(cx),
172 },
173 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
174 ButtonStyle::Subtle => ButtonLikeStyles {
175 background: cx.theme().colors().ghost_element_background,
176 border_color: transparent_black(),
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: transparent_black(),
183 label_color: Color::Default.color(cx),
184 icon_color: Color::Default.color(cx),
185 },
186 }
187 }
188
189 pub(crate) fn hovered(
190 self,
191 elevation: Option<Elevation>,
192 cx: &mut WindowContext,
193 ) -> ButtonLikeStyles {
194 let mut filled_background = element_bg_from_elevation(elevation, cx);
195 filled_background.fade_out(0.92);
196
197 match self {
198 ButtonStyle::Filled => ButtonLikeStyles {
199 background: filled_background,
200 border_color: transparent_black(),
201 label_color: Color::Default.color(cx),
202 icon_color: Color::Default.color(cx),
203 },
204 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
205 ButtonStyle::Subtle => ButtonLikeStyles {
206 background: cx.theme().colors().ghost_element_hover,
207 border_color: transparent_black(),
208 label_color: Color::Default.color(cx),
209 icon_color: Color::Default.color(cx),
210 },
211 ButtonStyle::Transparent => ButtonLikeStyles {
212 background: transparent_black(),
213 border_color: transparent_black(),
214 // TODO: These are not great
215 label_color: Color::Muted.color(cx),
216 // TODO: These are not great
217 icon_color: Color::Muted.color(cx),
218 },
219 }
220 }
221
222 pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
223 match self {
224 ButtonStyle::Filled => ButtonLikeStyles {
225 background: cx.theme().colors().element_active,
226 border_color: transparent_black(),
227 label_color: Color::Default.color(cx),
228 icon_color: Color::Default.color(cx),
229 },
230 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
231 ButtonStyle::Subtle => ButtonLikeStyles {
232 background: cx.theme().colors().ghost_element_active,
233 border_color: transparent_black(),
234 label_color: Color::Default.color(cx),
235 icon_color: Color::Default.color(cx),
236 },
237 ButtonStyle::Transparent => ButtonLikeStyles {
238 background: transparent_black(),
239 border_color: transparent_black(),
240 // TODO: These are not great
241 label_color: Color::Muted.color(cx),
242 // TODO: These are not great
243 icon_color: Color::Muted.color(cx),
244 },
245 }
246 }
247
248 #[allow(unused)]
249 pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
250 match self {
251 ButtonStyle::Filled => ButtonLikeStyles {
252 background: cx.theme().colors().element_background,
253 border_color: cx.theme().colors().border_focused,
254 label_color: Color::Default.color(cx),
255 icon_color: Color::Default.color(cx),
256 },
257 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
258 ButtonStyle::Subtle => ButtonLikeStyles {
259 background: cx.theme().colors().ghost_element_background,
260 border_color: cx.theme().colors().border_focused,
261 label_color: Color::Default.color(cx),
262 icon_color: Color::Default.color(cx),
263 },
264 ButtonStyle::Transparent => ButtonLikeStyles {
265 background: transparent_black(),
266 border_color: cx.theme().colors().border_focused,
267 label_color: Color::Accent.color(cx),
268 icon_color: Color::Accent.color(cx),
269 },
270 }
271 }
272
273 #[allow(unused)]
274 pub(crate) fn disabled(
275 self,
276 elevation: Option<Elevation>,
277 cx: &mut WindowContext,
278 ) -> ButtonLikeStyles {
279 let filled_background = element_bg_from_elevation(elevation, cx).fade_out(0.82);
280
281 match self {
282 ButtonStyle::Filled => ButtonLikeStyles {
283 background: cx.theme().colors().element_disabled,
284 border_color: cx.theme().colors().border_disabled,
285 label_color: Color::Disabled.color(cx),
286 icon_color: Color::Disabled.color(cx),
287 },
288 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
289 ButtonStyle::Subtle => ButtonLikeStyles {
290 background: cx.theme().colors().ghost_element_disabled,
291 border_color: cx.theme().colors().border_disabled,
292 label_color: Color::Disabled.color(cx),
293 icon_color: Color::Disabled.color(cx),
294 },
295 ButtonStyle::Transparent => ButtonLikeStyles {
296 background: transparent_black(),
297 border_color: transparent_black(),
298 label_color: Color::Disabled.color(cx),
299 icon_color: Color::Disabled.color(cx),
300 },
301 }
302 }
303}
304
305/// The height of a button.
306///
307/// Can also be used to size non-button elements to align with [`Button`]s.
308#[derive(Default, PartialEq, Clone, Copy)]
309pub enum ButtonSize {
310 Large,
311 #[default]
312 Default,
313 Compact,
314 None,
315}
316
317impl ButtonSize {
318 pub fn rems(self) -> Rems {
319 match self {
320 ButtonSize::Large => rems_from_px(32.),
321 ButtonSize::Default => rems_from_px(22.),
322 ButtonSize::Compact => rems_from_px(18.),
323 ButtonSize::None => rems_from_px(16.),
324 }
325 }
326}
327
328/// A button-like element that can be used to create a custom button when
329/// prebuilt buttons are not sufficient. Use this sparingly, as it is
330/// unconstrained and may make the UI feel less consistent.
331///
332/// This is also used to build the prebuilt buttons.
333#[derive(IntoElement)]
334pub struct ButtonLike {
335 pub(super) base: Div,
336 id: ElementId,
337 pub(super) style: ButtonStyle,
338 pub(super) disabled: bool,
339 pub(super) selected: bool,
340 pub(super) selected_style: Option<ButtonStyle>,
341 pub(super) width: Option<DefiniteLength>,
342 pub(super) height: Option<DefiniteLength>,
343 pub(super) layer: Option<Elevation>,
344 size: ButtonSize,
345 rounding: Option<ButtonLikeRounding>,
346 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
347 cursor_style: CursorStyle,
348 on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
349 children: SmallVec<[AnyElement; 2]>,
350}
351
352impl ButtonLike {
353 pub fn new(id: impl Into<ElementId>) -> Self {
354 Self {
355 base: div(),
356 id: id.into(),
357 style: ButtonStyle::default(),
358 disabled: false,
359 selected: false,
360 selected_style: None,
361 width: None,
362 height: None,
363 size: ButtonSize::Default,
364 rounding: Some(ButtonLikeRounding::All),
365 tooltip: None,
366 children: SmallVec::new(),
367 cursor_style: CursorStyle::PointingHand,
368 on_click: None,
369 layer: None,
370 }
371 }
372
373 pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
374 self.height = Some(height);
375 self
376 }
377
378 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
379 self.rounding = rounding.into();
380 self
381 }
382}
383
384impl Disableable for ButtonLike {
385 fn disabled(mut self, disabled: bool) -> Self {
386 self.disabled = disabled;
387 self
388 }
389}
390
391impl Selectable for ButtonLike {
392 fn selected(mut self, selected: bool) -> Self {
393 self.selected = selected;
394 self
395 }
396}
397
398impl SelectableButton for ButtonLike {
399 fn selected_style(mut self, style: ButtonStyle) -> Self {
400 self.selected_style = Some(style);
401 self
402 }
403}
404
405impl Clickable for ButtonLike {
406 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
407 self.on_click = Some(Box::new(handler));
408 self
409 }
410
411 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
412 self.cursor_style = cursor_style;
413 self
414 }
415}
416
417impl FixedWidth for ButtonLike {
418 fn width(mut self, width: DefiniteLength) -> Self {
419 self.width = Some(width);
420 self
421 }
422
423 fn full_width(mut self) -> Self {
424 self.width = Some(relative(1.));
425 self
426 }
427}
428
429impl ButtonCommon for ButtonLike {
430 fn id(&self) -> &ElementId {
431 &self.id
432 }
433
434 fn style(mut self, style: ButtonStyle) -> Self {
435 self.style = style;
436 self
437 }
438
439 fn size(mut self, size: ButtonSize) -> Self {
440 self.size = size;
441 self
442 }
443
444 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
445 self.tooltip = Some(Box::new(tooltip));
446 self
447 }
448
449 fn layer(mut self, elevation: ElevationIndex) -> Self {
450 self.layer = Some(elevation.into());
451 self
452 }
453}
454
455impl VisibleOnHover for ButtonLike {
456 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
457 self.base = self.base.visible_on_hover(group_name);
458 self
459 }
460}
461
462impl ParentElement for ButtonLike {
463 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
464 self.children.extend(elements)
465 }
466}
467
468impl RenderOnce for ButtonLike {
469 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
470 let style = self
471 .selected_style
472 .filter(|_| self.selected)
473 .unwrap_or(self.style);
474
475 self.base
476 .h_flex()
477 .id(self.id.clone())
478 .group("")
479 .flex_none()
480 .h(self.height.unwrap_or(self.size.rems().into()))
481 .when_some(self.width, |this, width| this.w(width).justify_center())
482 .when_some(self.rounding, |this, rounding| match rounding {
483 ButtonLikeRounding::All => this.rounded_md(),
484 ButtonLikeRounding::Left => this.rounded_l_md(),
485 ButtonLikeRounding::Right => this.rounded_r_md(),
486 })
487 .gap(Spacing::Small.rems(cx))
488 .map(|this| match self.size {
489 ButtonSize::Large => this.px(Spacing::Medium.rems(cx)),
490 ButtonSize::Default | ButtonSize::Compact => this.px(Spacing::Small.rems(cx)),
491 ButtonSize::None => this,
492 })
493 .bg(style.enabled(self.layer, cx).background)
494 .when(self.disabled, |this| this.cursor_not_allowed())
495 .when(!self.disabled, |this| {
496 this.cursor_pointer()
497 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
498 .active(|active| active.bg(style.active(cx).background))
499 })
500 .when_some(
501 self.on_click.filter(|_| !self.disabled),
502 |this, on_click| {
503 this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
504 .on_click(move |event, cx| {
505 cx.stop_propagation();
506 (on_click)(event, cx)
507 })
508 },
509 )
510 .when(!self.selected, |this| {
511 this.when_some(self.tooltip, |this, tooltip| {
512 this.tooltip(move |cx| tooltip(cx))
513 })
514 })
515 .children(self.children)
516 }
517}