1use std::sync::Arc;
2
3use gpui2::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
4
5use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor};
6use crate::{prelude::*, LineHeightStyle};
7
8#[derive(Default, PartialEq, Clone, Copy)]
9pub enum IconPosition {
10 #[default]
11 Left,
12 Right,
13}
14
15#[derive(Default, Copy, Clone, PartialEq)]
16pub enum ButtonVariant {
17 #[default]
18 Ghost,
19 Filled,
20}
21
22impl ButtonVariant {
23 pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
24 let theme = theme(cx);
25
26 match self {
27 ButtonVariant::Ghost => theme.ghost_element,
28 ButtonVariant::Filled => theme.filled_element,
29 }
30 }
31
32 pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
33 let theme = theme(cx);
34
35 match self {
36 ButtonVariant::Ghost => theme.ghost_element_hover,
37 ButtonVariant::Filled => theme.filled_element_hover,
38 }
39 }
40
41 pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
42 let theme = theme(cx);
43
44 match self {
45 ButtonVariant::Ghost => theme.ghost_element_active,
46 ButtonVariant::Filled => theme.filled_element_active,
47 }
48 }
49}
50
51pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + Send + Sync>;
52
53struct ButtonHandlers<V: 'static> {
54 click: Option<ClickHandler<V>>,
55}
56
57unsafe impl<S> Send for ButtonHandlers<S> {}
58unsafe impl<S> Sync for ButtonHandlers<S> {}
59
60impl<V: 'static> Default for ButtonHandlers<V> {
61 fn default() -> Self {
62 Self { click: None }
63 }
64}
65
66#[derive(Component)]
67pub struct Button<V: 'static> {
68 disabled: bool,
69 handlers: ButtonHandlers<V>,
70 icon: Option<Icon>,
71 icon_position: Option<IconPosition>,
72 label: SharedString,
73 variant: ButtonVariant,
74 width: Option<DefiniteLength>,
75}
76
77impl<V: 'static> Button<V> {
78 pub fn new(label: impl Into<SharedString>) -> Self {
79 Self {
80 disabled: false,
81 handlers: ButtonHandlers::default(),
82 icon: None,
83 icon_position: None,
84 label: label.into(),
85 variant: Default::default(),
86 width: Default::default(),
87 }
88 }
89
90 pub fn ghost(label: impl Into<SharedString>) -> Self {
91 Self::new(label).variant(ButtonVariant::Ghost)
92 }
93
94 pub fn variant(mut self, variant: ButtonVariant) -> Self {
95 self.variant = variant;
96 self
97 }
98
99 pub fn icon(mut self, icon: Icon) -> Self {
100 self.icon = Some(icon);
101 self
102 }
103
104 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
105 if self.icon.is_none() {
106 panic!("An icon must be present if an icon_position is provided.");
107 }
108 self.icon_position = Some(icon_position);
109 self
110 }
111
112 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
113 self.width = width;
114 self
115 }
116
117 pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
118 self.handlers.click = Some(handler);
119 self
120 }
121
122 pub fn disabled(mut self, disabled: bool) -> Self {
123 self.disabled = disabled;
124 self
125 }
126
127 fn label_color(&self) -> LabelColor {
128 if self.disabled {
129 LabelColor::Disabled
130 } else {
131 Default::default()
132 }
133 }
134
135 fn icon_color(&self) -> IconColor {
136 if self.disabled {
137 IconColor::Disabled
138 } else {
139 Default::default()
140 }
141 }
142
143 fn render_label(&self) -> Label {
144 Label::new(self.label.clone())
145 .color(self.label_color())
146 .line_height_style(LineHeightStyle::UILabel)
147 }
148
149 fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
150 self.icon.map(|i| IconElement::new(i).color(icon_color))
151 }
152
153 pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
154 let icon_color = self.icon_color();
155
156 let mut button = h_stack()
157 .relative()
158 .id(SharedString::from(format!("{}", self.label)))
159 .p_1()
160 .text_size(ui_size(cx, 1.))
161 .rounded_md()
162 .bg(self.variant.bg_color(cx))
163 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
164 .active(|style| style.bg(self.variant.bg_color_active(cx)));
165
166 match (self.icon, self.icon_position) {
167 (Some(_), Some(IconPosition::Left)) => {
168 button = button
169 .gap_1()
170 .child(self.render_label())
171 .children(self.render_icon(icon_color))
172 }
173 (Some(_), Some(IconPosition::Right)) => {
174 button = button
175 .gap_1()
176 .children(self.render_icon(icon_color))
177 .child(self.render_label())
178 }
179 (_, _) => button = button.child(self.render_label()),
180 }
181
182 if let Some(width) = self.width {
183 button = button.w(width).justify_center();
184 }
185
186 if let Some(click_handler) = self.handlers.click.clone() {
187 button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
188 click_handler(state, cx);
189 });
190 }
191
192 button
193 }
194}
195
196#[derive(Component)]
197pub struct ButtonGroup<V: 'static> {
198 buttons: Vec<Button<V>>,
199}
200
201impl<V: 'static> ButtonGroup<V> {
202 pub fn new(buttons: Vec<Button<V>>) -> Self {
203 Self { buttons }
204 }
205
206 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
207 let mut el = h_stack().text_size(ui_size(cx, 1.));
208
209 for button in self.buttons {
210 el = el.child(button.render(_view, cx));
211 }
212
213 el
214 }
215}
216
217#[cfg(feature = "stories")]
218pub use stories::*;
219
220#[cfg(feature = "stories")]
221mod stories {
222 use gpui2::rems;
223 use strum::IntoEnumIterator;
224
225 use crate::{h_stack, v_stack, LabelColor, Story};
226
227 use super::*;
228
229 #[derive(Component)]
230 pub struct ButtonStory;
231
232 impl ButtonStory {
233 pub fn new() -> Self {
234 Self
235 }
236
237 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
238 let states = InteractionState::iter();
239
240 Story::container(cx)
241 .child(Story::title_for::<_, Button<V>>(cx))
242 .child(
243 div()
244 .flex()
245 .gap_8()
246 .child(
247 div()
248 .child(Story::label(cx, "Ghost (Default)"))
249 .child(h_stack().gap_2().children(states.clone().map(|state| {
250 v_stack()
251 .gap_1()
252 .child(
253 Label::new(state.to_string()).color(LabelColor::Muted),
254 )
255 .child(
256 Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
257 )
258 })))
259 .child(Story::label(cx, "Ghost – Left Icon"))
260 .child(h_stack().gap_2().children(states.clone().map(|state| {
261 v_stack()
262 .gap_1()
263 .child(
264 Label::new(state.to_string()).color(LabelColor::Muted),
265 )
266 .child(
267 Button::new("Label")
268 .variant(ButtonVariant::Ghost)
269 .icon(Icon::Plus)
270 .icon_position(IconPosition::Left), // .state(state),
271 )
272 })))
273 .child(Story::label(cx, "Ghost – Right Icon"))
274 .child(h_stack().gap_2().children(states.clone().map(|state| {
275 v_stack()
276 .gap_1()
277 .child(
278 Label::new(state.to_string()).color(LabelColor::Muted),
279 )
280 .child(
281 Button::new("Label")
282 .variant(ButtonVariant::Ghost)
283 .icon(Icon::Plus)
284 .icon_position(IconPosition::Right), // .state(state),
285 )
286 }))),
287 )
288 .child(
289 div()
290 .child(Story::label(cx, "Filled"))
291 .child(h_stack().gap_2().children(states.clone().map(|state| {
292 v_stack()
293 .gap_1()
294 .child(
295 Label::new(state.to_string()).color(LabelColor::Muted),
296 )
297 .child(
298 Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
299 )
300 })))
301 .child(Story::label(cx, "Filled – Left Button"))
302 .child(h_stack().gap_2().children(states.clone().map(|state| {
303 v_stack()
304 .gap_1()
305 .child(
306 Label::new(state.to_string()).color(LabelColor::Muted),
307 )
308 .child(
309 Button::new("Label")
310 .variant(ButtonVariant::Filled)
311 .icon(Icon::Plus)
312 .icon_position(IconPosition::Left), // .state(state),
313 )
314 })))
315 .child(Story::label(cx, "Filled – Right Button"))
316 .child(h_stack().gap_2().children(states.clone().map(|state| {
317 v_stack()
318 .gap_1()
319 .child(
320 Label::new(state.to_string()).color(LabelColor::Muted),
321 )
322 .child(
323 Button::new("Label")
324 .variant(ButtonVariant::Filled)
325 .icon(Icon::Plus)
326 .icon_position(IconPosition::Right), // .state(state),
327 )
328 }))),
329 )
330 .child(
331 div()
332 .child(Story::label(cx, "Fixed With"))
333 .child(h_stack().gap_2().children(states.clone().map(|state| {
334 v_stack()
335 .gap_1()
336 .child(
337 Label::new(state.to_string()).color(LabelColor::Muted),
338 )
339 .child(
340 Button::new("Label")
341 .variant(ButtonVariant::Filled)
342 // .state(state)
343 .width(Some(rems(6.).into())),
344 )
345 })))
346 .child(Story::label(cx, "Fixed With – Left Icon"))
347 .child(h_stack().gap_2().children(states.clone().map(|state| {
348 v_stack()
349 .gap_1()
350 .child(
351 Label::new(state.to_string()).color(LabelColor::Muted),
352 )
353 .child(
354 Button::new("Label")
355 .variant(ButtonVariant::Filled)
356 // .state(state)
357 .icon(Icon::Plus)
358 .icon_position(IconPosition::Left)
359 .width(Some(rems(6.).into())),
360 )
361 })))
362 .child(Story::label(cx, "Fixed With – Right Icon"))
363 .child(h_stack().gap_2().children(states.clone().map(|state| {
364 v_stack()
365 .gap_1()
366 .child(
367 Label::new(state.to_string()).color(LabelColor::Muted),
368 )
369 .child(
370 Button::new("Label")
371 .variant(ButtonVariant::Filled)
372 // .state(state)
373 .icon(Icon::Plus)
374 .icon_position(IconPosition::Right)
375 .width(Some(rems(6.).into())),
376 )
377 }))),
378 ),
379 )
380 .child(Story::label(cx, "Button with `on_click`"))
381 .child(
382 Button::new("Label")
383 .variant(ButtonVariant::Ghost)
384 .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
385 )
386 }
387 }
388}