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