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