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