1use gpui::{relative, DefiniteLength, MouseButton};
2use gpui::{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 foreground 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 pub fn rems(self) -> Rems {
280 match self {
281 ButtonSize::Large => rems_from_px(32.),
282 ButtonSize::Default => rems_from_px(22.),
283 ButtonSize::Compact => rems_from_px(18.),
284 ButtonSize::None => rems_from_px(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 pub 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 pub(super) height: Option<DefiniteLength>,
304 size: ButtonSize,
305 rounding: Option<ButtonLikeRounding>,
306 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
307 on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
308 children: SmallVec<[AnyElement; 2]>,
309}
310
311impl ButtonLike {
312 pub fn new(id: impl Into<ElementId>) -> Self {
313 Self {
314 base: div(),
315 id: id.into(),
316 style: ButtonStyle::default(),
317 disabled: false,
318 selected: false,
319 selected_style: None,
320 width: None,
321 height: None,
322 size: ButtonSize::Default,
323 rounding: Some(ButtonLikeRounding::All),
324 tooltip: None,
325 children: SmallVec::new(),
326 on_click: None,
327 }
328 }
329
330 pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
331 self.height = Some(height);
332 self
333 }
334
335 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
336 self.rounding = rounding.into();
337 self
338 }
339}
340
341impl Disableable for ButtonLike {
342 fn disabled(mut self, disabled: bool) -> Self {
343 self.disabled = disabled;
344 self
345 }
346}
347
348impl Selectable for ButtonLike {
349 fn selected(mut self, selected: bool) -> Self {
350 self.selected = selected;
351 self
352 }
353}
354
355impl SelectableButton for ButtonLike {
356 fn selected_style(mut self, style: ButtonStyle) -> Self {
357 self.selected_style = Some(style);
358 self
359 }
360}
361
362impl Clickable for ButtonLike {
363 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
364 self.on_click = Some(Box::new(handler));
365 self
366 }
367}
368
369impl FixedWidth for ButtonLike {
370 fn width(mut self, width: DefiniteLength) -> Self {
371 self.width = Some(width);
372 self
373 }
374
375 fn full_width(mut self) -> Self {
376 self.width = Some(relative(1.));
377 self
378 }
379}
380
381impl ButtonCommon for ButtonLike {
382 fn id(&self) -> &ElementId {
383 &self.id
384 }
385
386 fn style(mut self, style: ButtonStyle) -> Self {
387 self.style = style;
388 self
389 }
390
391 fn size(mut self, size: ButtonSize) -> Self {
392 self.size = size;
393 self
394 }
395
396 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
397 self.tooltip = Some(Box::new(tooltip));
398 self
399 }
400}
401
402impl VisibleOnHover for ButtonLike {
403 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
404 self.base = self.base.visible_on_hover(group_name);
405 self
406 }
407}
408
409impl ParentElement for ButtonLike {
410 fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
411 self.children.extend(elements)
412 }
413}
414
415impl RenderOnce for ButtonLike {
416 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
417 let style = self
418 .selected_style
419 .filter(|_| self.selected)
420 .unwrap_or(self.style);
421
422 self.base
423 .h_flex()
424 .id(self.id.clone())
425 .group("")
426 .flex_none()
427 .h(self.height.unwrap_or(self.size.rems().into()))
428 .when_some(self.width, |this, width| this.w(width).justify_center())
429 .when_some(self.rounding, |this, rounding| match rounding {
430 ButtonLikeRounding::All => this.rounded_md(),
431 ButtonLikeRounding::Left => this.rounded_l_md(),
432 ButtonLikeRounding::Right => this.rounded_r_md(),
433 })
434 .gap_1()
435 .map(|this| match self.size {
436 ButtonSize::Large => this.px_2(),
437 ButtonSize::Default | ButtonSize::Compact => this.px_1(),
438 ButtonSize::None => this,
439 })
440 .bg(style.enabled(cx).background)
441 .when(self.disabled, |this| this.cursor_not_allowed())
442 .when(!self.disabled, |this| {
443 this.cursor_pointer()
444 .hover(|hover| hover.bg(style.hovered(cx).background))
445 .active(|active| active.bg(style.active(cx).background))
446 })
447 .when_some(
448 self.on_click.filter(|_| !self.disabled),
449 |this, on_click| {
450 this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
451 .on_click(move |event, cx| {
452 cx.stop_propagation();
453 (on_click)(event, cx)
454 })
455 },
456 )
457 .when_some(self.tooltip, |this, tooltip| {
458 this.tooltip(move |cx| tooltip(cx))
459 })
460 .children(self.children)
461 }
462}