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