1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use gpui2::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
5
6use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor};
7use crate::{prelude::*, LineHeightStyle};
8
9#[derive(Default, PartialEq, Clone, Copy)]
10pub enum IconPosition {
11 #[default]
12 Left,
13 Right,
14}
15
16#[derive(Default, Copy, Clone, PartialEq)]
17pub enum ButtonVariant {
18 #[default]
19 Ghost,
20 Filled,
21}
22
23impl ButtonVariant {
24 pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
25 let theme = theme(cx);
26
27 match self {
28 ButtonVariant::Ghost => theme.ghost_element,
29 ButtonVariant::Filled => theme.filled_element,
30 }
31 }
32
33 pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
34 let theme = theme(cx);
35
36 match self {
37 ButtonVariant::Ghost => theme.ghost_element_hover,
38 ButtonVariant::Filled => theme.filled_element_hover,
39 }
40 }
41
42 pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
43 let theme = theme(cx);
44
45 match self {
46 ButtonVariant::Ghost => theme.ghost_element_active,
47 ButtonVariant::Filled => theme.filled_element_active,
48 }
49 }
50}
51
52pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + 'static + Send + Sync>;
53
54struct ButtonHandlers<S: 'static + Send + Sync> {
55 click: Option<ClickHandler<S>>,
56}
57
58impl<S: 'static + Send + Sync> Default for ButtonHandlers<S> {
59 fn default() -> Self {
60 Self { click: None }
61 }
62}
63
64#[derive(Element)]
65pub struct Button<S: 'static + Send + Sync> {
66 state_type: PhantomData<S>,
67 disabled: bool,
68 handlers: ButtonHandlers<S>,
69 icon: Option<Icon>,
70 icon_position: Option<IconPosition>,
71 label: SharedString,
72 variant: ButtonVariant,
73 width: Option<DefiniteLength>,
74}
75
76impl<S: 'static + Send + Sync> Button<S> {
77 pub fn new(label: impl Into<SharedString>) -> Self {
78 Self {
79 state_type: PhantomData,
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<S>) -> 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<S> {
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<S>> {
150 self.icon.map(|i| IconElement::new(i).color(icon_color))
151 }
152
153 pub fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl IntoAnyElement<S> {
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(Element)]
197pub struct ButtonGroup<S: 'static + Send + Sync> {
198 state_type: PhantomData<S>,
199 buttons: Vec<Button<S>>,
200}
201
202impl<S: 'static + Send + Sync> ButtonGroup<S> {
203 pub fn new(buttons: Vec<Button<S>>) -> Self {
204 Self {
205 state_type: PhantomData,
206 buttons,
207 }
208 }
209
210 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl IntoAnyElement<S> {
211 let mut el = h_stack().text_size(ui_size(cx, 1.));
212
213 for button in &mut self.buttons {
214 el = el.child(button.render(_view, cx));
215 }
216
217 el
218 }
219}
220
221#[cfg(feature = "stories")]
222pub use stories::*;
223
224#[cfg(feature = "stories")]
225mod stories {
226 use gpui2::rems;
227 use strum::IntoEnumIterator;
228
229 use crate::{h_stack, v_stack, LabelColor, Story};
230
231 use super::*;
232
233 #[derive(Element)]
234 pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
235 state_type: PhantomData<S>,
236 }
237
238 impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
239 pub fn new() -> Self {
240 Self {
241 state_type: PhantomData,
242 }
243 }
244
245 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl IntoAnyElement<S> {
246 let states = InteractionState::iter();
247
248 Story::container(cx)
249 .child(Story::title_for::<_, Button<S>>(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}