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