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