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