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 color: Option<LabelColor>,
91}
92
93impl<V: 'static> Button<V> {
94 pub fn new(label: impl Into<SharedString>) -> Self {
95 Self {
96 disabled: false,
97 handlers: ButtonHandlers::default(),
98 icon: None,
99 icon_position: None,
100 label: label.into(),
101 variant: Default::default(),
102 width: Default::default(),
103 color: None,
104 }
105 }
106
107 pub fn ghost(label: impl Into<SharedString>) -> Self {
108 Self::new(label).variant(ButtonVariant::Ghost)
109 }
110
111 pub fn variant(mut self, variant: ButtonVariant) -> Self {
112 self.variant = variant;
113 self
114 }
115
116 pub fn icon(mut self, icon: Icon) -> Self {
117 self.icon = Some(icon);
118 self
119 }
120
121 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
122 if self.icon.is_none() {
123 panic!("An icon must be present if an icon_position is provided.");
124 }
125 self.icon_position = Some(icon_position);
126 self
127 }
128
129 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
130 self.width = width;
131 self
132 }
133
134 pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
135 self.handlers.click = Some(handler);
136 self
137 }
138
139 pub fn disabled(mut self, disabled: bool) -> Self {
140 self.disabled = disabled;
141 self
142 }
143
144 pub fn color(mut self, color: Option<LabelColor>) -> Self {
145 self.color = color;
146 self
147 }
148
149 pub fn label_color(&self, color: Option<LabelColor>) -> LabelColor {
150 if self.disabled {
151 LabelColor::Disabled
152 } else if let Some(color) = color {
153 color
154 } else {
155 Default::default()
156 }
157 }
158
159 fn render_label(&self, color: LabelColor) -> Label {
160 Label::new(self.label.clone())
161 .color(color)
162 .line_height_style(LineHeightStyle::UILabel)
163 }
164
165 fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
166 self.icon.map(|i| IconElement::new(i).color(icon_color))
167 }
168
169 pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
170 let (icon_color, label_color) = match (self.disabled, self.color) {
171 (true, _) => (IconColor::Disabled, LabelColor::Disabled),
172 (_, None) => (IconColor::Default, LabelColor::Default),
173 (_, Some(color)) => (IconColor::from(color), color),
174 };
175
176 let mut button = h_stack()
177 .id(SharedString::from(format!("{}", self.label)))
178 .relative()
179 .p_1()
180 .text_ui()
181 .rounded_md()
182 .bg(self.variant.bg_color(cx))
183 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
184 .active(|style| style.bg(self.variant.bg_color_active(cx)));
185
186 match (self.icon, self.icon_position) {
187 (Some(_), Some(IconPosition::Left)) => {
188 button = button
189 .gap_1()
190 .child(self.render_label(label_color))
191 .children(self.render_icon(icon_color))
192 }
193 (Some(_), Some(IconPosition::Right)) => {
194 button = button
195 .gap_1()
196 .children(self.render_icon(icon_color))
197 .child(self.render_label(label_color))
198 }
199 (_, _) => button = button.child(self.render_label(label_color)),
200 }
201
202 if let Some(width) = self.width {
203 button = button.w(width).justify_center();
204 }
205
206 if let Some(click_handler) = self.handlers.click.clone() {
207 button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
208 click_handler(state, cx);
209 });
210 }
211
212 button
213 }
214}
215
216#[derive(Component)]
217pub struct ButtonGroup<V: 'static> {
218 buttons: Vec<Button<V>>,
219}
220
221impl<V: 'static> ButtonGroup<V> {
222 pub fn new(buttons: Vec<Button<V>>) -> Self {
223 Self { buttons }
224 }
225
226 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
227 let mut el = h_stack().text_ui();
228
229 for button in self.buttons {
230 el = el.child(button.render(_view, cx));
231 }
232
233 el
234 }
235}
236
237#[cfg(feature = "stories")]
238pub use stories::*;
239
240#[cfg(feature = "stories")]
241mod stories {
242 use super::*;
243 use crate::{h_stack, v_stack, LabelColor, Story};
244 use gpui::{rems, Div, Render};
245 use strum::IntoEnumIterator;
246
247 pub struct ButtonStory;
248
249 impl Render for ButtonStory {
250 type Element = Div<Self>;
251
252 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
253 let states = InteractionState::iter();
254
255 Story::container(cx)
256 .child(Story::title_for::<_, Button<Self>>(cx))
257 .child(
258 div()
259 .flex()
260 .gap_8()
261 .child(
262 div()
263 .child(Story::label(cx, "Ghost (Default)"))
264 .child(h_stack().gap_2().children(states.clone().map(|state| {
265 v_stack()
266 .gap_1()
267 .child(
268 Label::new(state.to_string()).color(LabelColor::Muted),
269 )
270 .child(
271 Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
272 )
273 })))
274 .child(Story::label(cx, "Ghost – Left Icon"))
275 .child(h_stack().gap_2().children(states.clone().map(|state| {
276 v_stack()
277 .gap_1()
278 .child(
279 Label::new(state.to_string()).color(LabelColor::Muted),
280 )
281 .child(
282 Button::new("Label")
283 .variant(ButtonVariant::Ghost)
284 .icon(Icon::Plus)
285 .icon_position(IconPosition::Left), // .state(state),
286 )
287 })))
288 .child(Story::label(cx, "Ghost – Right Icon"))
289 .child(h_stack().gap_2().children(states.clone().map(|state| {
290 v_stack()
291 .gap_1()
292 .child(
293 Label::new(state.to_string()).color(LabelColor::Muted),
294 )
295 .child(
296 Button::new("Label")
297 .variant(ButtonVariant::Ghost)
298 .icon(Icon::Plus)
299 .icon_position(IconPosition::Right), // .state(state),
300 )
301 }))),
302 )
303 .child(
304 div()
305 .child(Story::label(cx, "Filled"))
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()).color(LabelColor::Muted),
311 )
312 .child(
313 Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
314 )
315 })))
316 .child(Story::label(cx, "Filled – Left Button"))
317 .child(h_stack().gap_2().children(states.clone().map(|state| {
318 v_stack()
319 .gap_1()
320 .child(
321 Label::new(state.to_string()).color(LabelColor::Muted),
322 )
323 .child(
324 Button::new("Label")
325 .variant(ButtonVariant::Filled)
326 .icon(Icon::Plus)
327 .icon_position(IconPosition::Left), // .state(state),
328 )
329 })))
330 .child(Story::label(cx, "Filled – Right Button"))
331 .child(h_stack().gap_2().children(states.clone().map(|state| {
332 v_stack()
333 .gap_1()
334 .child(
335 Label::new(state.to_string()).color(LabelColor::Muted),
336 )
337 .child(
338 Button::new("Label")
339 .variant(ButtonVariant::Filled)
340 .icon(Icon::Plus)
341 .icon_position(IconPosition::Right), // .state(state),
342 )
343 }))),
344 )
345 .child(
346 div()
347 .child(Story::label(cx, "Fixed With"))
348 .child(h_stack().gap_2().children(states.clone().map(|state| {
349 v_stack()
350 .gap_1()
351 .child(
352 Label::new(state.to_string()).color(LabelColor::Muted),
353 )
354 .child(
355 Button::new("Label")
356 .variant(ButtonVariant::Filled)
357 // .state(state)
358 .width(Some(rems(6.).into())),
359 )
360 })))
361 .child(Story::label(cx, "Fixed With – Left Icon"))
362 .child(h_stack().gap_2().children(states.clone().map(|state| {
363 v_stack()
364 .gap_1()
365 .child(
366 Label::new(state.to_string()).color(LabelColor::Muted),
367 )
368 .child(
369 Button::new("Label")
370 .variant(ButtonVariant::Filled)
371 // .state(state)
372 .icon(Icon::Plus)
373 .icon_position(IconPosition::Left)
374 .width(Some(rems(6.).into())),
375 )
376 })))
377 .child(Story::label(cx, "Fixed With – Right Icon"))
378 .child(h_stack().gap_2().children(states.clone().map(|state| {
379 v_stack()
380 .gap_1()
381 .child(
382 Label::new(state.to_string()).color(LabelColor::Muted),
383 )
384 .child(
385 Button::new("Label")
386 .variant(ButtonVariant::Filled)
387 // .state(state)
388 .icon(Icon::Plus)
389 .icon_position(IconPosition::Right)
390 .width(Some(rems(6.).into())),
391 )
392 }))),
393 ),
394 )
395 .child(Story::label(cx, "Button with `on_click`"))
396 .child(
397 Button::new("Label")
398 .variant(ButtonVariant::Ghost)
399 .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
400 )
401 }
402 }
403}