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