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 fn new_rounded_left(id: impl Into<ElementId>) -> Self {
374 Self::new(id).rounding(ButtonLikeRounding::Left)
375 }
376
377 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
378 Self::new(id).rounding(ButtonLikeRounding::Right)
379 }
380
381 pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
382 self.height = Some(height);
383 self
384 }
385
386 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
387 self.rounding = rounding.into();
388 self
389 }
390}
391
392impl Disableable for ButtonLike {
393 fn disabled(mut self, disabled: bool) -> Self {
394 self.disabled = disabled;
395 self
396 }
397}
398
399impl Selectable for ButtonLike {
400 fn selected(mut self, selected: bool) -> Self {
401 self.selected = selected;
402 self
403 }
404}
405
406impl SelectableButton for ButtonLike {
407 fn selected_style(mut self, style: ButtonStyle) -> Self {
408 self.selected_style = Some(style);
409 self
410 }
411}
412
413impl Clickable for ButtonLike {
414 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
415 self.on_click = Some(Box::new(handler));
416 self
417 }
418
419 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
420 self.cursor_style = cursor_style;
421 self
422 }
423}
424
425impl FixedWidth for ButtonLike {
426 fn width(mut self, width: DefiniteLength) -> Self {
427 self.width = Some(width);
428 self
429 }
430
431 fn full_width(mut self) -> Self {
432 self.width = Some(relative(1.));
433 self
434 }
435}
436
437impl ButtonCommon for ButtonLike {
438 fn id(&self) -> &ElementId {
439 &self.id
440 }
441
442 fn style(mut self, style: ButtonStyle) -> Self {
443 self.style = style;
444 self
445 }
446
447 fn size(mut self, size: ButtonSize) -> Self {
448 self.size = size;
449 self
450 }
451
452 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
453 self.tooltip = Some(Box::new(tooltip));
454 self
455 }
456
457 fn layer(mut self, elevation: ElevationIndex) -> Self {
458 self.layer = Some(elevation.into());
459 self
460 }
461}
462
463impl VisibleOnHover for ButtonLike {
464 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
465 self.base = self.base.visible_on_hover(group_name);
466 self
467 }
468}
469
470impl ParentElement for ButtonLike {
471 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
472 self.children.extend(elements)
473 }
474}
475
476impl RenderOnce for ButtonLike {
477 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
478 let style = self
479 .selected_style
480 .filter(|_| self.selected)
481 .unwrap_or(self.style);
482
483 self.base
484 .h_flex()
485 .id(self.id.clone())
486 .group("")
487 .flex_none()
488 .h(self.height.unwrap_or(self.size.rems().into()))
489 .when_some(self.width, |this, width| this.w(width).justify_center())
490 .when_some(self.rounding, |this, rounding| match rounding {
491 ButtonLikeRounding::All => this.rounded_md(),
492 ButtonLikeRounding::Left => this.rounded_l_md(),
493 ButtonLikeRounding::Right => this.rounded_r_md(),
494 })
495 .gap(Spacing::Small.rems(cx))
496 .map(|this| match self.size {
497 ButtonSize::Large => this.px(Spacing::Medium.rems(cx)),
498 ButtonSize::Default | ButtonSize::Compact => this.px(Spacing::Small.rems(cx)),
499 ButtonSize::None => this,
500 })
501 .bg(style.enabled(self.layer, cx).background)
502 .when(self.disabled, |this| this.cursor_not_allowed())
503 .when(!self.disabled, |this| {
504 this.cursor_pointer()
505 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
506 .active(|active| active.bg(style.active(cx).background))
507 })
508 .when_some(
509 self.on_click.filter(|_| !self.disabled),
510 |this, on_click| {
511 this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
512 .on_click(move |event, cx| {
513 cx.stop_propagation();
514 (on_click)(event, cx)
515 })
516 },
517 )
518 .when(!self.selected, |this| {
519 this.when_some(self.tooltip, |this, tooltip| {
520 this.tooltip(move |cx| tooltip(cx))
521 })
522 })
523 .children(self.children)
524 }
525}