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