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(
154 &mut self,
155 _view: &mut S,
156 cx: &mut ViewContext<S>,
157 ) -> impl Element<S> {
158 let icon_color = self.icon_color();
159
160 let mut button = h_stack()
161 .relative()
162 .id(SharedString::from(format!("{}", self.label)))
163 .p_1()
164 .text_size(ui_size(cx, 1.))
165 .rounded_md()
166 .bg(self.variant.bg_color(cx))
167 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
168 .active(|style| style.bg(self.variant.bg_color_active(cx)));
169
170 match (self.icon, self.icon_position) {
171 (Some(_), Some(IconPosition::Left)) => {
172 button = button
173 .gap_1()
174 .child(self.render_label())
175 .children(self.render_icon(icon_color))
176 }
177 (Some(_), Some(IconPosition::Right)) => {
178 button = button
179 .gap_1()
180 .children(self.render_icon(icon_color))
181 .child(self.render_label())
182 }
183 (_, _) => button = button.child(self.render_label()),
184 }
185
186 if let Some(width) = self.width {
187 button = button.w(width).justify_center();
188 }
189
190 if let Some(click_handler) = self.handlers.click.clone() {
191 button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
192 click_handler(state, cx);
193 });
194 }
195
196 button
197 }
198}
199
200#[derive(Element)]
201pub struct ButtonGroup<S: 'static + Send + Sync> {
202 state_type: PhantomData<S>,
203 buttons: Vec<Button<S>>,
204}
205
206impl<S: 'static + Send + Sync> ButtonGroup<S> {
207 pub fn new(buttons: Vec<Button<S>>) -> Self {
208 Self {
209 state_type: PhantomData,
210 buttons,
211 }
212 }
213
214 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<S> {
215 let mut el = h_stack().text_size(ui_size(cx, 1.));
216
217 for button in &mut self.buttons {
218 el = el.child(button.render(_view, cx));
219 }
220
221 el
222 }
223}
224
225#[cfg(feature = "stories")]
226pub use stories::*;
227
228#[cfg(feature = "stories")]
229mod stories {
230 use gpui2::rems;
231 use strum::IntoEnumIterator;
232
233 use crate::{h_stack, v_stack, LabelColor, Story};
234
235 use super::*;
236
237 #[derive(Element)]
238 pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
239 state_type: PhantomData<S>,
240 }
241
242 impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
243 pub fn new() -> Self {
244 Self {
245 state_type: PhantomData,
246 }
247 }
248
249 fn render(
250 &mut self,
251 _view: &mut S,
252 cx: &mut ViewContext<S>,
253 ) -> impl Element<S> {
254 let states = InteractionState::iter();
255
256 Story::container(cx)
257 .child(Story::title_for::<_, Button<S>>(cx))
258 .child(
259 div()
260 .flex()
261 .gap_8()
262 .child(
263 div()
264 .child(Story::label(cx, "Ghost (Default)"))
265 .child(h_stack().gap_2().children(states.clone().map(|state| {
266 v_stack()
267 .gap_1()
268 .child(
269 Label::new(state.to_string()).color(LabelColor::Muted),
270 )
271 .child(
272 Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
273 )
274 })))
275 .child(Story::label(cx, "Ghost – Left Icon"))
276 .child(h_stack().gap_2().children(states.clone().map(|state| {
277 v_stack()
278 .gap_1()
279 .child(
280 Label::new(state.to_string()).color(LabelColor::Muted),
281 )
282 .child(
283 Button::new("Label")
284 .variant(ButtonVariant::Ghost)
285 .icon(Icon::Plus)
286 .icon_position(IconPosition::Left), // .state(state),
287 )
288 })))
289 .child(Story::label(cx, "Ghost – Right Icon"))
290 .child(h_stack().gap_2().children(states.clone().map(|state| {
291 v_stack()
292 .gap_1()
293 .child(
294 Label::new(state.to_string()).color(LabelColor::Muted),
295 )
296 .child(
297 Button::new("Label")
298 .variant(ButtonVariant::Ghost)
299 .icon(Icon::Plus)
300 .icon_position(IconPosition::Right), // .state(state),
301 )
302 }))),
303 )
304 .child(
305 div()
306 .child(Story::label(cx, "Filled"))
307 .child(h_stack().gap_2().children(states.clone().map(|state| {
308 v_stack()
309 .gap_1()
310 .child(
311 Label::new(state.to_string()).color(LabelColor::Muted),
312 )
313 .child(
314 Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
315 )
316 })))
317 .child(Story::label(cx, "Filled – Left Button"))
318 .child(h_stack().gap_2().children(states.clone().map(|state| {
319 v_stack()
320 .gap_1()
321 .child(
322 Label::new(state.to_string()).color(LabelColor::Muted),
323 )
324 .child(
325 Button::new("Label")
326 .variant(ButtonVariant::Filled)
327 .icon(Icon::Plus)
328 .icon_position(IconPosition::Left), // .state(state),
329 )
330 })))
331 .child(Story::label(cx, "Filled – Right Button"))
332 .child(h_stack().gap_2().children(states.clone().map(|state| {
333 v_stack()
334 .gap_1()
335 .child(
336 Label::new(state.to_string()).color(LabelColor::Muted),
337 )
338 .child(
339 Button::new("Label")
340 .variant(ButtonVariant::Filled)
341 .icon(Icon::Plus)
342 .icon_position(IconPosition::Right), // .state(state),
343 )
344 }))),
345 )
346 .child(
347 div()
348 .child(Story::label(cx, "Fixed With"))
349 .child(h_stack().gap_2().children(states.clone().map(|state| {
350 v_stack()
351 .gap_1()
352 .child(
353 Label::new(state.to_string()).color(LabelColor::Muted),
354 )
355 .child(
356 Button::new("Label")
357 .variant(ButtonVariant::Filled)
358 // .state(state)
359 .width(Some(rems(6.).into())),
360 )
361 })))
362 .child(Story::label(cx, "Fixed With – Left Icon"))
363 .child(h_stack().gap_2().children(states.clone().map(|state| {
364 v_stack()
365 .gap_1()
366 .child(
367 Label::new(state.to_string()).color(LabelColor::Muted),
368 )
369 .child(
370 Button::new("Label")
371 .variant(ButtonVariant::Filled)
372 // .state(state)
373 .icon(Icon::Plus)
374 .icon_position(IconPosition::Left)
375 .width(Some(rems(6.).into())),
376 )
377 })))
378 .child(Story::label(cx, "Fixed With – Right Icon"))
379 .child(h_stack().gap_2().children(states.clone().map(|state| {
380 v_stack()
381 .gap_1()
382 .child(
383 Label::new(state.to_string()).color(LabelColor::Muted),
384 )
385 .child(
386 Button::new("Label")
387 .variant(ButtonVariant::Filled)
388 // .state(state)
389 .icon(Icon::Plus)
390 .icon_position(IconPosition::Right)
391 .width(Some(rems(6.).into())),
392 )
393 }))),
394 ),
395 )
396 .child(Story::label(cx, "Button with `on_click`"))
397 .child(
398 Button::new("Label")
399 .variant(ButtonVariant::Ghost)
400 .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
401 )
402 }
403 }
404}