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 self.label_color()
133 }
134 }
135
136 fn icon_color(&self) -> IconColor {
137 if self.disabled {
138 IconColor::Disabled
139 } else {
140 self.icon_color()
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")
276 .variant(ButtonVariant::Ghost)
277 .state(state),
278 )
279 })))
280 .child(Story::label(cx, "Ghost – Left Icon"))
281 .child(h_stack().gap_2().children(states.clone().map(|state| {
282 v_stack()
283 .gap_1()
284 .child(
285 Label::new(state.to_string()).color(LabelColor::Muted),
286 )
287 .child(
288 Button::new("Label")
289 .variant(ButtonVariant::Ghost)
290 .icon(Icon::Plus)
291 .icon_position(IconPosition::Left)
292 .state(state),
293 )
294 })))
295 .child(Story::label(cx, "Ghost – Right Icon"))
296 .child(h_stack().gap_2().children(states.clone().map(|state| {
297 v_stack()
298 .gap_1()
299 .child(
300 Label::new(state.to_string()).color(LabelColor::Muted),
301 )
302 .child(
303 Button::new("Label")
304 .variant(ButtonVariant::Ghost)
305 .icon(Icon::Plus)
306 .icon_position(IconPosition::Right)
307 .state(state),
308 )
309 }))),
310 )
311 .child(
312 div()
313 .child(Story::label(cx, "Filled"))
314 .child(h_stack().gap_2().children(states.clone().map(|state| {
315 v_stack()
316 .gap_1()
317 .child(
318 Label::new(state.to_string()).color(LabelColor::Muted),
319 )
320 .child(
321 Button::new("Label")
322 .variant(ButtonVariant::Filled)
323 .state(state),
324 )
325 })))
326 .child(Story::label(cx, "Filled – Left Button"))
327 .child(h_stack().gap_2().children(states.clone().map(|state| {
328 v_stack()
329 .gap_1()
330 .child(
331 Label::new(state.to_string()).color(LabelColor::Muted),
332 )
333 .child(
334 Button::new("Label")
335 .variant(ButtonVariant::Filled)
336 .icon(Icon::Plus)
337 .icon_position(IconPosition::Left)
338 .state(state),
339 )
340 })))
341 .child(Story::label(cx, "Filled – Right Button"))
342 .child(h_stack().gap_2().children(states.clone().map(|state| {
343 v_stack()
344 .gap_1()
345 .child(
346 Label::new(state.to_string()).color(LabelColor::Muted),
347 )
348 .child(
349 Button::new("Label")
350 .variant(ButtonVariant::Filled)
351 .icon(Icon::Plus)
352 .icon_position(IconPosition::Right)
353 .state(state),
354 )
355 }))),
356 )
357 .child(
358 div()
359 .child(Story::label(cx, "Fixed With"))
360 .child(h_stack().gap_2().children(states.clone().map(|state| {
361 v_stack()
362 .gap_1()
363 .child(
364 Label::new(state.to_string()).color(LabelColor::Muted),
365 )
366 .child(
367 Button::new("Label")
368 .variant(ButtonVariant::Filled)
369 .state(state)
370 .width(Some(rems(6.).into())),
371 )
372 })))
373 .child(Story::label(cx, "Fixed With – Left Icon"))
374 .child(h_stack().gap_2().children(states.clone().map(|state| {
375 v_stack()
376 .gap_1()
377 .child(
378 Label::new(state.to_string()).color(LabelColor::Muted),
379 )
380 .child(
381 Button::new("Label")
382 .variant(ButtonVariant::Filled)
383 .state(state)
384 .icon(Icon::Plus)
385 .icon_position(IconPosition::Left)
386 .width(Some(rems(6.).into())),
387 )
388 })))
389 .child(Story::label(cx, "Fixed With – Right Icon"))
390 .child(h_stack().gap_2().children(states.clone().map(|state| {
391 v_stack()
392 .gap_1()
393 .child(
394 Label::new(state.to_string()).color(LabelColor::Muted),
395 )
396 .child(
397 Button::new("Label")
398 .variant(ButtonVariant::Filled)
399 .state(state)
400 .icon(Icon::Plus)
401 .icon_position(IconPosition::Right)
402 .width(Some(rems(6.).into())),
403 )
404 }))),
405 ),
406 )
407 .child(Story::label(cx, "Button with `on_click`"))
408 .child(
409 Button::new("Label")
410 .variant(ButtonVariant::Ghost)
411 .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
412 )
413 }
414 }
415}