1use std::sync::Arc;
2
3use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
4
5use crate::prelude::*;
6use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
7
8/// Provides the flexibility to use either a standard
9/// button or an icon button in a given context.
10pub enum ButtonOrIconButton<V: 'static> {
11 Button(Button<V>),
12 IconButton(IconButton<V>),
13}
14
15impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
16 fn from(value: Button<V>) -> Self {
17 Self::Button(value)
18 }
19}
20
21impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
22 fn from(value: IconButton<V>) -> Self {
23 Self::IconButton(value)
24 }
25}
26
27#[derive(Default, PartialEq, Clone, Copy)]
28pub enum IconPosition {
29 #[default]
30 Left,
31 Right,
32}
33
34#[derive(Default, Copy, Clone, PartialEq)]
35pub enum ButtonVariant {
36 #[default]
37 Ghost,
38 Filled,
39}
40
41impl ButtonVariant {
42 pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
43 match self {
44 ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
45 ButtonVariant::Filled => cx.theme().colors().element_background,
46 }
47 }
48
49 pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
50 match self {
51 ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
52 ButtonVariant::Filled => cx.theme().colors().element_hover,
53 }
54 }
55
56 pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
57 match self {
58 ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
59 ButtonVariant::Filled => cx.theme().colors().element_active,
60 }
61 }
62}
63
64pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
65
66struct ButtonHandlers<V: 'static> {
67 click: Option<ClickHandler<V>>,
68}
69
70unsafe impl<S> Send for ButtonHandlers<S> {}
71unsafe impl<S> Sync for ButtonHandlers<S> {}
72
73impl<V: 'static> Default for ButtonHandlers<V> {
74 fn default() -> Self {
75 Self { click: None }
76 }
77}
78
79#[derive(Component)]
80pub struct Button<V: 'static> {
81 disabled: bool,
82 handlers: ButtonHandlers<V>,
83 icon: Option<Icon>,
84 icon_position: Option<IconPosition>,
85 label: SharedString,
86 variant: ButtonVariant,
87 width: Option<DefiniteLength>,
88 color: Option<TextColor>,
89}
90
91impl<V: 'static> Button<V> {
92 pub fn new(label: impl Into<SharedString>) -> Self {
93 Self {
94 disabled: false,
95 handlers: ButtonHandlers::default(),
96 icon: None,
97 icon_position: None,
98 label: label.into(),
99 variant: Default::default(),
100 width: Default::default(),
101 color: None,
102 }
103 }
104
105 pub fn ghost(label: impl Into<SharedString>) -> Self {
106 Self::new(label).variant(ButtonVariant::Ghost)
107 }
108
109 pub fn variant(mut self, variant: ButtonVariant) -> Self {
110 self.variant = variant;
111 self
112 }
113
114 pub fn icon(mut self, icon: Icon) -> Self {
115 self.icon = Some(icon);
116 self
117 }
118
119 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
120 if self.icon.is_none() {
121 panic!("An icon must be present if an icon_position is provided.");
122 }
123 self.icon_position = Some(icon_position);
124 self
125 }
126
127 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
128 self.width = width;
129 self
130 }
131
132 pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
133 self.handlers.click = Some(handler);
134 self
135 }
136
137 pub fn disabled(mut self, disabled: bool) -> Self {
138 self.disabled = disabled;
139 self
140 }
141
142 pub fn color(mut self, color: Option<TextColor>) -> Self {
143 self.color = color;
144 self
145 }
146
147 pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
148 if self.disabled {
149 TextColor::Disabled
150 } else if let Some(color) = color {
151 color
152 } else {
153 Default::default()
154 }
155 }
156
157 fn render_label(&self, color: TextColor) -> Label {
158 Label::new(self.label.clone())
159 .color(color)
160 .line_height_style(LineHeightStyle::UILabel)
161 }
162
163 fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
164 self.icon.map(|i| IconElement::new(i).color(icon_color))
165 }
166
167 pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
168 let (icon_color, label_color) = match (self.disabled, self.color) {
169 (true, _) => (TextColor::Disabled, TextColor::Disabled),
170 (_, None) => (TextColor::Default, TextColor::Default),
171 (_, Some(color)) => (TextColor::from(color), color),
172 };
173
174 let mut button = h_stack()
175 .id(SharedString::from(format!("{}", self.label)))
176 .relative()
177 .p_1()
178 .text_ui()
179 .rounded_md()
180 .bg(self.variant.bg_color(cx))
181 .cursor_pointer()
182 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
183 .active(|style| style.bg(self.variant.bg_color_active(cx)));
184
185 match (self.icon, self.icon_position) {
186 (Some(_), Some(IconPosition::Left)) => {
187 button = button
188 .gap_1()
189 .child(self.render_label(label_color))
190 .children(self.render_icon(icon_color))
191 }
192 (Some(_), Some(IconPosition::Right)) => {
193 button = button
194 .gap_1()
195 .children(self.render_icon(icon_color))
196 .child(self.render_label(label_color))
197 }
198 (_, _) => button = button.child(self.render_label(label_color)),
199 }
200
201 if let Some(width) = self.width {
202 button = button.w(width).justify_center();
203 }
204
205 if let Some(click_handler) = self.handlers.click.clone() {
206 button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
207 click_handler(state, cx);
208 });
209 }
210
211 button
212 }
213}
214
215#[derive(Component)]
216pub struct ButtonGroup<V: 'static> {
217 buttons: Vec<Button<V>>,
218}
219
220impl<V: 'static> ButtonGroup<V> {
221 pub fn new(buttons: Vec<Button<V>>) -> Self {
222 Self { buttons }
223 }
224
225 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
226 let mut el = h_stack().text_ui();
227
228 for button in self.buttons {
229 el = el.child(button.render(_view, cx));
230 }
231
232 el
233 }
234}
235
236#[cfg(feature = "stories")]
237pub use stories::*;
238
239#[cfg(feature = "stories")]
240mod stories {
241 use super::*;
242 use crate::{h_stack, v_stack, Story, TextColor};
243 use gpui::{rems, Div, Render};
244 use strum::IntoEnumIterator;
245
246 pub struct ButtonStory;
247
248 impl Render for ButtonStory {
249 type Element = Div<Self>;
250
251 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
252 let states = InteractionState::iter();
253
254 Story::container(cx)
255 .child(Story::title_for::<_, Button<Self>>(cx))
256 .child(
257 div()
258 .flex()
259 .gap_8()
260 .child(
261 div()
262 .child(Story::label(cx, "Ghost (Default)"))
263 .child(h_stack().gap_2().children(states.clone().map(|state| {
264 v_stack()
265 .gap_1()
266 .child(
267 Label::new(state.to_string()).color(TextColor::Muted),
268 )
269 .child(
270 Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
271 )
272 })))
273 .child(Story::label(cx, "Ghost – Left Icon"))
274 .child(h_stack().gap_2().children(states.clone().map(|state| {
275 v_stack()
276 .gap_1()
277 .child(
278 Label::new(state.to_string()).color(TextColor::Muted),
279 )
280 .child(
281 Button::new("Label")
282 .variant(ButtonVariant::Ghost)
283 .icon(Icon::Plus)
284 .icon_position(IconPosition::Left), // .state(state),
285 )
286 })))
287 .child(Story::label(cx, "Ghost – Right Icon"))
288 .child(h_stack().gap_2().children(states.clone().map(|state| {
289 v_stack()
290 .gap_1()
291 .child(
292 Label::new(state.to_string()).color(TextColor::Muted),
293 )
294 .child(
295 Button::new("Label")
296 .variant(ButtonVariant::Ghost)
297 .icon(Icon::Plus)
298 .icon_position(IconPosition::Right), // .state(state),
299 )
300 }))),
301 )
302 .child(
303 div()
304 .child(Story::label(cx, "Filled"))
305 .child(h_stack().gap_2().children(states.clone().map(|state| {
306 v_stack()
307 .gap_1()
308 .child(
309 Label::new(state.to_string()).color(TextColor::Muted),
310 )
311 .child(
312 Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
313 )
314 })))
315 .child(Story::label(cx, "Filled – Left Button"))
316 .child(h_stack().gap_2().children(states.clone().map(|state| {
317 v_stack()
318 .gap_1()
319 .child(
320 Label::new(state.to_string()).color(TextColor::Muted),
321 )
322 .child(
323 Button::new("Label")
324 .variant(ButtonVariant::Filled)
325 .icon(Icon::Plus)
326 .icon_position(IconPosition::Left), // .state(state),
327 )
328 })))
329 .child(Story::label(cx, "Filled – Right Button"))
330 .child(h_stack().gap_2().children(states.clone().map(|state| {
331 v_stack()
332 .gap_1()
333 .child(
334 Label::new(state.to_string()).color(TextColor::Muted),
335 )
336 .child(
337 Button::new("Label")
338 .variant(ButtonVariant::Filled)
339 .icon(Icon::Plus)
340 .icon_position(IconPosition::Right), // .state(state),
341 )
342 }))),
343 )
344 .child(
345 div()
346 .child(Story::label(cx, "Fixed With"))
347 .child(h_stack().gap_2().children(states.clone().map(|state| {
348 v_stack()
349 .gap_1()
350 .child(
351 Label::new(state.to_string()).color(TextColor::Muted),
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()).color(TextColor::Muted),
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()).color(TextColor::Muted),
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}