1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use gpui2::{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
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> {
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> 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 .color(self.label_color())
142 .line_height_style(LineHeightStyle::UILabel)
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 pub fn render(
150 &mut self,
151 _view: &mut S,
152 cx: &mut ViewContext<S>,
153 ) -> impl Element<ViewState = S> {
154 let icon_color = self.icon_color();
155 let border_color = self.border_color(cx);
156 let settings = user_settings(cx);
157
158 let mut el = h_stack()
159 .p_1()
160 .text_size(ui_size(cx, 1.))
161 .rounded_md()
162 .border()
163 .border_color(border_color)
164 .bg(self.background_color(cx))
165 .hover(|style| {
166 let color = ThemeColor::new(cx);
167
168 style.bg(match self.variant {
169 ButtonVariant::Ghost => color.ghost_element_hover,
170 ButtonVariant::Filled => color.filled_element_hover,
171 })
172 });
173
174 match (self.icon, self.icon_position) {
175 (Some(_), Some(IconPosition::Left)) => {
176 el = el
177 .gap_1()
178 .child(self.render_label())
179 .children(self.render_icon(icon_color))
180 }
181 (Some(_), Some(IconPosition::Right)) => {
182 el = el
183 .gap_1()
184 .children(self.render_icon(icon_color))
185 .child(self.render_label())
186 }
187 (_, _) => el = el.child(self.render_label()),
188 }
189
190 if let Some(width) = self.width {
191 el = el.w(width).justify_center();
192 }
193
194 if let Some(click_handler) = self.handlers.click.clone() {
195 el = el.on_mouse_down(MouseButton::Left, move |state, event, cx| {
196 click_handler(state, cx);
197 });
198 }
199
200 el
201 }
202}
203
204#[derive(Element)]
205pub struct ButtonGroup<S: 'static + Send + Sync> {
206 state_type: PhantomData<S>,
207 buttons: Vec<Button<S>>,
208}
209
210impl<S: 'static + Send + Sync> ButtonGroup<S> {
211 pub fn new(buttons: Vec<Button<S>>) -> Self {
212 Self {
213 state_type: PhantomData,
214 buttons,
215 }
216 }
217
218 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
219 let mut el = h_stack().text_size(ui_size(cx, 1.));
220
221 for button in &mut self.buttons {
222 el = el.child(button.render(_view, cx));
223 }
224
225 el
226 }
227}
228
229#[cfg(feature = "stories")]
230pub use stories::*;
231
232#[cfg(feature = "stories")]
233mod stories {
234 use gpui2::rems;
235 use strum::IntoEnumIterator;
236
237 use crate::{h_stack, v_stack, LabelColor, Story};
238
239 use super::*;
240
241 #[derive(Element)]
242 pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
243 state_type: PhantomData<S>,
244 }
245
246 impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
247 pub fn new() -> Self {
248 Self {
249 state_type: PhantomData,
250 }
251 }
252
253 fn render(
254 &mut self,
255 _view: &mut S,
256 cx: &mut ViewContext<S>,
257 ) -> impl Element<ViewState = S> {
258 let states = InteractionState::iter();
259
260 Story::container(cx)
261 .child(Story::title_for::<_, Button<S>>(cx))
262 .child(
263 div()
264 .flex()
265 .gap_8()
266 .child(
267 div()
268 .child(Story::label(cx, "Ghost (Default)"))
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()).color(LabelColor::Muted),
274 )
275 .child(
276 Button::new("Label")
277 .variant(ButtonVariant::Ghost)
278 .state(state),
279 )
280 })))
281 .child(Story::label(cx, "Ghost – Left Icon"))
282 .child(h_stack().gap_2().children(states.clone().map(|state| {
283 v_stack()
284 .gap_1()
285 .child(
286 Label::new(state.to_string()).color(LabelColor::Muted),
287 )
288 .child(
289 Button::new("Label")
290 .variant(ButtonVariant::Ghost)
291 .icon(Icon::Plus)
292 .icon_position(IconPosition::Left)
293 .state(state),
294 )
295 })))
296 .child(Story::label(cx, "Ghost – Right Icon"))
297 .child(h_stack().gap_2().children(states.clone().map(|state| {
298 v_stack()
299 .gap_1()
300 .child(
301 Label::new(state.to_string()).color(LabelColor::Muted),
302 )
303 .child(
304 Button::new("Label")
305 .variant(ButtonVariant::Ghost)
306 .icon(Icon::Plus)
307 .icon_position(IconPosition::Right)
308 .state(state),
309 )
310 }))),
311 )
312 .child(
313 div()
314 .child(Story::label(cx, "Filled"))
315 .child(h_stack().gap_2().children(states.clone().map(|state| {
316 v_stack()
317 .gap_1()
318 .child(
319 Label::new(state.to_string()).color(LabelColor::Muted),
320 )
321 .child(
322 Button::new("Label")
323 .variant(ButtonVariant::Filled)
324 .state(state),
325 )
326 })))
327 .child(Story::label(cx, "Filled – Left Button"))
328 .child(h_stack().gap_2().children(states.clone().map(|state| {
329 v_stack()
330 .gap_1()
331 .child(
332 Label::new(state.to_string()).color(LabelColor::Muted),
333 )
334 .child(
335 Button::new("Label")
336 .variant(ButtonVariant::Filled)
337 .icon(Icon::Plus)
338 .icon_position(IconPosition::Left)
339 .state(state),
340 )
341 })))
342 .child(Story::label(cx, "Filled – Right Button"))
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()).color(LabelColor::Muted),
348 )
349 .child(
350 Button::new("Label")
351 .variant(ButtonVariant::Filled)
352 .icon(Icon::Plus)
353 .icon_position(IconPosition::Right)
354 .state(state),
355 )
356 }))),
357 )
358 .child(
359 div()
360 .child(Story::label(cx, "Fixed With"))
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()).color(LabelColor::Muted),
366 )
367 .child(
368 Button::new("Label")
369 .variant(ButtonVariant::Filled)
370 .state(state)
371 .width(Some(rems(6.).into())),
372 )
373 })))
374 .child(Story::label(cx, "Fixed With – Left 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()).color(LabelColor::Muted),
380 )
381 .child(
382 Button::new("Label")
383 .variant(ButtonVariant::Filled)
384 .state(state)
385 .icon(Icon::Plus)
386 .icon_position(IconPosition::Left)
387 .width(Some(rems(6.).into())),
388 )
389 })))
390 .child(Story::label(cx, "Fixed With – Right Icon"))
391 .child(h_stack().gap_2().children(states.clone().map(|state| {
392 v_stack()
393 .gap_1()
394 .child(
395 Label::new(state.to_string()).color(LabelColor::Muted),
396 )
397 .child(
398 Button::new("Label")
399 .variant(ButtonVariant::Filled)
400 .state(state)
401 .icon(Icon::Plus)
402 .icon_position(IconPosition::Right)
403 .width(Some(rems(6.).into())),
404 )
405 }))),
406 ),
407 )
408 .child(Story::label(cx, "Button with `on_click`"))
409 .child(
410 Button::new("Label")
411 .variant(ButtonVariant::Ghost)
412 .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
413 )
414 }
415 }
416}