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