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