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