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