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