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