1#![allow(missing_docs)]
2use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
3use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
4use smallvec::SmallVec;
5
6use crate::{prelude::*, DynamicSpacing, ElevationIndex};
7
8/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
9pub trait SelectableButton: Toggleable {
10 fn selected_style(self, style: ButtonStyle) -> Self;
11}
12
13/// A common set of traits all buttons must implement.
14pub trait ButtonCommon: Clickable + Disableable {
15 /// A unique element ID to identify the button.
16 fn id(&self) -> &ElementId;
17
18 /// The visual style of the button.
19 ///
20 /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
21 /// for an emphasized button.
22 fn style(self, style: ButtonStyle) -> Self;
23
24 /// The size of the button.
25 ///
26 /// Most buttons will use the default size.
27 ///
28 /// [`ButtonSize`] can also be used to help build non-button elements
29 /// that are consistently sized with buttons.
30 fn size(self, size: ButtonSize) -> Self;
31
32 /// The tooltip that shows when a user hovers over the button.
33 ///
34 /// Nearly all interactable elements should have a tooltip. Some example
35 /// exceptions might a scroll bar, or a slider.
36 fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self;
37
38 fn layer(self, elevation: ElevationIndex) -> Self;
39}
40
41#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
42pub enum IconPosition {
43 #[default]
44 Start,
45 End,
46}
47
48#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
49pub enum TintColor {
50 #[default]
51 Accent,
52 Error,
53 Warning,
54 Success,
55}
56
57impl TintColor {
58 fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles {
59 match self {
60 TintColor::Accent => ButtonLikeStyles {
61 background: cx.theme().status().info_background,
62 border_color: cx.theme().status().info_border,
63 label_color: cx.theme().colors().text,
64 icon_color: cx.theme().colors().text,
65 },
66 TintColor::Error => ButtonLikeStyles {
67 background: cx.theme().status().error_background,
68 border_color: cx.theme().status().error_border,
69 label_color: cx.theme().colors().text,
70 icon_color: cx.theme().colors().text,
71 },
72 TintColor::Warning => ButtonLikeStyles {
73 background: cx.theme().status().warning_background,
74 border_color: cx.theme().status().warning_border,
75 label_color: cx.theme().colors().text,
76 icon_color: cx.theme().colors().text,
77 },
78 TintColor::Success => ButtonLikeStyles {
79 background: cx.theme().status().success_background,
80 border_color: cx.theme().status().success_border,
81 label_color: cx.theme().colors().text,
82 icon_color: cx.theme().colors().text,
83 },
84 }
85 }
86}
87
88impl From<TintColor> for Color {
89 fn from(tint: TintColor) -> Self {
90 match tint {
91 TintColor::Accent => Color::Accent,
92 TintColor::Error => Color::Error,
93 TintColor::Warning => Color::Warning,
94 TintColor::Success => Color::Success,
95 }
96 }
97}
98
99// Used to go from ButtonStyle -> Color through tint colors.
100impl From<ButtonStyle> for Color {
101 fn from(style: ButtonStyle) -> Self {
102 match style {
103 ButtonStyle::Tinted(tint) => tint.into(),
104 _ => Color::Default,
105 }
106 }
107}
108
109/// The visual appearance of a button.
110#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
111pub enum ButtonStyle {
112 /// A filled button with a solid background color. Provides emphasis versus
113 /// the more common subtle button.
114 Filled,
115
116 /// Used to emphasize a button in some way, like a selected state, or a semantic
117 /// coloring like an error or success button.
118 Tinted(TintColor),
119
120 /// The default button style, used for most buttons. Has a transparent background,
121 /// but has a background color to indicate states like hover and active.
122 #[default]
123 Subtle,
124
125 /// Used for buttons that only change foreground color on hover and active states.
126 ///
127 /// TODO: Better docs for this.
128 Transparent,
129}
130
131#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
132pub(crate) enum ButtonLikeRounding {
133 All,
134 Left,
135 Right,
136}
137
138#[derive(Debug, Clone)]
139pub(crate) struct ButtonLikeStyles {
140 pub background: Hsla,
141 #[allow(unused)]
142 pub border_color: Hsla,
143 #[allow(unused)]
144 pub label_color: Hsla,
145 #[allow(unused)]
146 pub icon_color: Hsla,
147}
148
149fn element_bg_from_elevation(elevation: Option<ElevationIndex>, cx: &mut App) -> Hsla {
150 match elevation {
151 Some(ElevationIndex::Background) => cx.theme().colors().element_background,
152 Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
153 Some(ElevationIndex::Surface) => cx.theme().colors().surface_background,
154 Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
155 _ => cx.theme().colors().element_background,
156 }
157}
158
159impl ButtonStyle {
160 pub(crate) fn enabled(
161 self,
162 elevation: Option<ElevationIndex>,
163
164 cx: &mut App,
165 ) -> ButtonLikeStyles {
166 match self {
167 ButtonStyle::Filled => ButtonLikeStyles {
168 background: element_bg_from_elevation(elevation, cx),
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<ElevationIndex>,
192
193 cx: &mut App,
194 ) -> ButtonLikeStyles {
195 match self {
196 ButtonStyle::Filled => {
197 let mut filled_background = element_bg_from_elevation(elevation, cx);
198 filled_background.fade_out(0.92);
199
200 ButtonLikeStyles {
201 background: filled_background,
202 border_color: transparent_black(),
203 label_color: Color::Default.color(cx),
204 icon_color: Color::Default.color(cx),
205 }
206 }
207 ButtonStyle::Tinted(tint) => {
208 let mut styles = tint.button_like_style(cx);
209 let theme = cx.theme();
210 styles.background = theme.darken(styles.background, 0.05, 0.2);
211 styles
212 }
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 App) -> 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, window: &mut Window, cx: &mut App) -> 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<ElevationIndex>,
285 window: &mut Window,
286 cx: &mut App,
287 ) -> ButtonLikeStyles {
288 match self {
289 ButtonStyle::Filled => ButtonLikeStyles {
290 background: cx.theme().colors().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::Tinted(tint) => tint.button_like_style(cx),
296 ButtonStyle::Subtle => ButtonLikeStyles {
297 background: cx.theme().colors().ghost_element_disabled,
298 border_color: cx.theme().colors().border_disabled,
299 label_color: Color::Disabled.color(cx),
300 icon_color: Color::Disabled.color(cx),
301 },
302 ButtonStyle::Transparent => ButtonLikeStyles {
303 background: transparent_black(),
304 border_color: transparent_black(),
305 label_color: Color::Disabled.color(cx),
306 icon_color: Color::Disabled.color(cx),
307 },
308 }
309 }
310}
311
312/// The height of a button.
313///
314/// Can also be used to size non-button elements to align with [`Button`]s.
315#[derive(Default, PartialEq, Clone, Copy)]
316pub enum ButtonSize {
317 Large,
318 #[default]
319 Default,
320 Compact,
321 None,
322}
323
324impl ButtonSize {
325 pub fn rems(self) -> Rems {
326 match self {
327 ButtonSize::Large => rems_from_px(32.),
328 ButtonSize::Default => rems_from_px(22.),
329 ButtonSize::Compact => rems_from_px(18.),
330 ButtonSize::None => rems_from_px(16.),
331 }
332 }
333}
334
335/// A button-like element that can be used to create a custom button when
336/// prebuilt buttons are not sufficient. Use this sparingly, as it is
337/// unconstrained and may make the UI feel less consistent.
338///
339/// This is also used to build the prebuilt buttons.
340#[derive(IntoElement)]
341pub struct ButtonLike {
342 pub(super) base: Div,
343 id: ElementId,
344 pub(super) style: ButtonStyle,
345 pub(super) disabled: bool,
346 pub(super) selected: bool,
347 pub(super) selected_style: Option<ButtonStyle>,
348 pub(super) width: Option<DefiniteLength>,
349 pub(super) height: Option<DefiniteLength>,
350 pub(super) layer: Option<ElevationIndex>,
351 size: ButtonSize,
352 rounding: Option<ButtonLikeRounding>,
353 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
354 cursor_style: CursorStyle,
355 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
356 children: SmallVec<[AnyElement; 2]>,
357}
358
359impl ButtonLike {
360 pub fn new(id: impl Into<ElementId>) -> Self {
361 Self {
362 base: div(),
363 id: id.into(),
364 style: ButtonStyle::default(),
365 disabled: false,
366 selected: false,
367 selected_style: None,
368 width: None,
369 height: None,
370 size: ButtonSize::Default,
371 rounding: Some(ButtonLikeRounding::All),
372 tooltip: None,
373 children: SmallVec::new(),
374 cursor_style: CursorStyle::PointingHand,
375 on_click: None,
376 layer: None,
377 }
378 }
379
380 pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
381 Self::new(id).rounding(ButtonLikeRounding::Left)
382 }
383
384 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
385 Self::new(id).rounding(ButtonLikeRounding::Right)
386 }
387
388 pub fn opacity(mut self, opacity: f32) -> Self {
389 self.base = self.base.opacity(opacity);
390 self
391 }
392
393 pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
394 self.height = Some(height);
395 self
396 }
397
398 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
399 self.rounding = rounding.into();
400 self
401 }
402}
403
404impl Disableable for ButtonLike {
405 fn disabled(mut self, disabled: bool) -> Self {
406 self.disabled = disabled;
407 self
408 }
409}
410
411impl Toggleable for ButtonLike {
412 fn toggle_state(mut self, selected: bool) -> Self {
413 self.selected = selected;
414 self
415 }
416}
417
418impl SelectableButton for ButtonLike {
419 fn selected_style(mut self, style: ButtonStyle) -> Self {
420 self.selected_style = Some(style);
421 self
422 }
423}
424
425impl Clickable for ButtonLike {
426 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
427 self.on_click = Some(Box::new(handler));
428 self
429 }
430
431 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
432 self.cursor_style = cursor_style;
433 self
434 }
435}
436
437impl FixedWidth for ButtonLike {
438 fn width(mut self, width: DefiniteLength) -> Self {
439 self.width = Some(width);
440 self
441 }
442
443 fn full_width(mut self) -> Self {
444 self.width = Some(relative(1.));
445 self
446 }
447}
448
449impl ButtonCommon for ButtonLike {
450 fn id(&self) -> &ElementId {
451 &self.id
452 }
453
454 fn style(mut self, style: ButtonStyle) -> Self {
455 self.style = style;
456 self
457 }
458
459 fn size(mut self, size: ButtonSize) -> Self {
460 self.size = size;
461 self
462 }
463
464 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
465 self.tooltip = Some(Box::new(tooltip));
466 self
467 }
468
469 fn layer(mut self, elevation: ElevationIndex) -> Self {
470 self.layer = Some(elevation);
471 self
472 }
473}
474
475impl VisibleOnHover for ButtonLike {
476 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
477 self.base = self.base.visible_on_hover(group_name);
478 self
479 }
480}
481
482impl ParentElement for ButtonLike {
483 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
484 self.children.extend(elements)
485 }
486}
487
488impl RenderOnce for ButtonLike {
489 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
490 let style = self
491 .selected_style
492 .filter(|_| self.selected)
493 .unwrap_or(self.style);
494
495 self.base
496 .h_flex()
497 .id(self.id.clone())
498 .font_ui(cx)
499 .group("")
500 .flex_none()
501 .h(self.height.unwrap_or(self.size.rems().into()))
502 .when_some(self.width, |this, width| this.w(width).justify_center())
503 .when_some(self.rounding, |this, rounding| match rounding {
504 ButtonLikeRounding::All => this.rounded_md(),
505 ButtonLikeRounding::Left => this.rounded_l_md(),
506 ButtonLikeRounding::Right => this.rounded_r_md(),
507 })
508 .gap(DynamicSpacing::Base04.rems(cx))
509 .map(|this| match self.size {
510 ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
511 ButtonSize::Default | ButtonSize::Compact => {
512 this.px(DynamicSpacing::Base04.rems(cx))
513 }
514 ButtonSize::None => this,
515 })
516 .bg(style.enabled(self.layer, cx).background)
517 .when(self.disabled, |this| this.cursor_not_allowed())
518 .when(!self.disabled, |this| {
519 this.cursor_pointer()
520 .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
521 .active(|active| active.bg(style.active(cx).background))
522 })
523 .when_some(
524 self.on_click.filter(|_| !self.disabled),
525 |this, on_click| {
526 this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
527 .on_click(move |event, window, cx| {
528 cx.stop_propagation();
529 (on_click)(event, window, cx)
530 })
531 },
532 )
533 .when_some(self.tooltip, |this, tooltip| {
534 this.tooltip(move |window, cx| tooltip(window, cx))
535 })
536 .children(self.children)
537 }
538}