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