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