1use gpui::{
2 ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
3};
4use std::rc::Rc;
5
6use crate::prelude::*;
7use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
8
9/// Provides the flexibility to use either a standard
10/// button or an icon button in a given context.
11pub enum ButtonOrIconButton {
12 Button(Button),
13 IconButton(IconButton),
14}
15
16impl From<Button> for ButtonOrIconButton {
17 fn from(value: Button) -> Self {
18 Self::Button(value)
19 }
20}
21
22impl From<IconButton> for ButtonOrIconButton {
23 fn from(value: IconButton) -> Self {
24 Self::IconButton(value)
25 }
26}
27
28#[derive(Default, PartialEq, Clone, Copy)]
29pub enum IconPosition {
30 #[default]
31 Left,
32 Right,
33}
34
35#[derive(Default, Copy, Clone, PartialEq)]
36pub enum ButtonVariant {
37 #[default]
38 Ghost,
39 Filled,
40}
41
42impl ButtonVariant {
43 pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
44 match self {
45 ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
46 ButtonVariant::Filled => cx.theme().colors().element_background,
47 }
48 }
49
50 pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
51 match self {
52 ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
53 ButtonVariant::Filled => cx.theme().colors().element_hover,
54 }
55 }
56
57 pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
58 match self {
59 ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
60 ButtonVariant::Filled => cx.theme().colors().element_active,
61 }
62 }
63}
64
65#[derive(IntoElement)]
66pub struct Button {
67 disabled: bool,
68 click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
69 icon: Option<Icon>,
70 icon_position: Option<IconPosition>,
71 label: SharedString,
72 variant: ButtonVariant,
73 width: Option<DefiniteLength>,
74 color: Option<Color>,
75}
76
77impl RenderOnce for Button {
78 type Rendered = gpui::Stateful<Div>;
79
80 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
81 let (icon_color, label_color) = match (self.disabled, self.color) {
82 (true, _) => (Color::Disabled, Color::Disabled),
83 (_, None) => (Color::Default, Color::Default),
84 (_, Some(color)) => (Color::from(color), color),
85 };
86
87 let mut button = h_stack()
88 .id(SharedString::from(format!("{}", self.label)))
89 .relative()
90 .p_1()
91 .text_ui()
92 .rounded_md()
93 .bg(self.variant.bg_color(cx))
94 .cursor_pointer()
95 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
96 .active(|style| style.bg(self.variant.bg_color_active(cx)));
97
98 match (self.icon, self.icon_position) {
99 (Some(_), Some(IconPosition::Left)) => {
100 button = button
101 .gap_1()
102 .child(self.render_label(label_color))
103 .children(self.render_icon(icon_color))
104 }
105 (Some(_), Some(IconPosition::Right)) => {
106 button = button
107 .gap_1()
108 .children(self.render_icon(icon_color))
109 .child(self.render_label(label_color))
110 }
111 (_, _) => button = button.child(self.render_label(label_color)),
112 }
113
114 if let Some(width) = self.width {
115 button = button.w(width).justify_center();
116 }
117
118 if let Some(click_handler) = self.click_handler.clone() {
119 button = button.on_click(move |event, cx| {
120 click_handler(event, cx);
121 });
122 }
123
124 button
125 }
126}
127
128impl Button {
129 pub fn new(label: impl Into<SharedString>) -> Self {
130 Self {
131 disabled: false,
132 click_handler: None,
133 icon: None,
134 icon_position: None,
135 label: label.into(),
136 variant: Default::default(),
137 width: Default::default(),
138 color: None,
139 }
140 }
141
142 pub fn ghost(label: impl Into<SharedString>) -> Self {
143 Self::new(label).variant(ButtonVariant::Ghost)
144 }
145
146 pub fn variant(mut self, variant: ButtonVariant) -> Self {
147 self.variant = variant;
148 self
149 }
150
151 pub fn icon(mut self, icon: Icon) -> Self {
152 self.icon = Some(icon);
153 self
154 }
155
156 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
157 if self.icon.is_none() {
158 panic!("An icon must be present if an icon_position is provided.");
159 }
160 self.icon_position = Some(icon_position);
161 self
162 }
163
164 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
165 self.width = width;
166 self
167 }
168
169 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
170 self.click_handler = Some(Rc::new(handler));
171 self
172 }
173
174 pub fn disabled(mut self, disabled: bool) -> Self {
175 self.disabled = disabled;
176 self
177 }
178
179 pub fn color(mut self, color: Option<Color>) -> Self {
180 self.color = color;
181 self
182 }
183
184 pub fn label_color(&self, color: Option<Color>) -> Color {
185 if self.disabled {
186 Color::Disabled
187 } else if let Some(color) = color {
188 color
189 } else {
190 Default::default()
191 }
192 }
193
194 fn render_label(&self, color: Color) -> Label {
195 Label::new(self.label.clone())
196 .color(color)
197 .line_height_style(LineHeightStyle::UILabel)
198 }
199
200 fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
201 self.icon.map(|i| IconElement::new(i).color(icon_color))
202 }
203}
204
205#[derive(IntoElement)]
206pub struct ButtonGroup {
207 buttons: Vec<Button>,
208}
209
210impl RenderOnce for ButtonGroup {
211 type Rendered = Div;
212
213 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
214 let mut group = h_stack();
215
216 for button in self.buttons.into_iter() {
217 group = group.child(button.render(cx));
218 }
219
220 group
221 }
222}
223
224impl ButtonGroup {
225 pub fn new(buttons: Vec<Button>) -> Self {
226 Self { buttons }
227 }
228}