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