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