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