ui/components/button/button.rs
1use crate::component_prelude::*;
2use gpui::{AnyElement, AnyView, DefiniteLength};
3use ui_macros::RegisterComponent;
4
5use crate::traits::animation_ext::CommonAnimationExt;
6use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, Label};
7use crate::{
8 Color, DynamicSpacing, ElevationIndex, KeyBinding, KeybindingPosition, TintColor, prelude::*,
9};
10
11/// An element that creates a button with a label and optional icons.
12///
13/// Common buttons:
14/// - Label, Icon + Label: [`Button`] (this component)
15/// - Icon only: [`IconButton`]
16/// - Custom: [`ButtonLike`]
17///
18/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
19/// [`ButtonLike`] directly.
20///
21/// # Examples
22///
23/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
24/// indicates what action will be performed when the button is clicked.
25///
26/// ```
27/// use ui::prelude::*;
28///
29/// Button::new("button_id", "Click me!")
30/// .on_click(|event, window, cx| {
31/// // Handle click event
32/// });
33/// ```
34///
35/// **A toggleable button**, is typically used in scenarios such as a toolbar,
36/// where the button's state indicates whether a feature is enabled or not, or
37/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
38///
39/// ```
40/// use ui::prelude::*;
41///
42/// Button::new("button_id", "Click me!")
43/// .start_icon(Icon::new(IconName::Check))
44/// .toggle_state(true)
45/// .on_click(|event, window, cx| {
46/// // Handle click event
47/// });
48/// ```
49///
50/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
51///
52/// ```
53/// use ui::prelude::*;
54/// use ui::TintColor;
55///
56/// Button::new("button_id", "Click me!")
57/// .toggle_state(true)
58/// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
59/// .on_click(|event, window, cx| {
60/// // Handle click event
61/// });
62/// ```
63/// This will create a button with a blue tinted background when selected.
64///
65/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container.
66/// The button's content, including text and icons, is centered by default.
67///
68/// ```
69/// use ui::prelude::*;
70///
71/// let button = Button::new("button_id", "Click me!")
72/// .full_width()
73/// .on_click(|event, window, cx| {
74/// // Handle click event
75/// });
76/// ```
77///
78#[derive(IntoElement, Documented, RegisterComponent)]
79pub struct Button {
80 base: ButtonLike,
81 label: SharedString,
82 label_color: Option<Color>,
83 label_size: Option<LabelSize>,
84 selected_label: Option<SharedString>,
85 selected_label_color: Option<Color>,
86 start_icon: Option<Icon>,
87 end_icon: Option<Icon>,
88 key_binding: Option<KeyBinding>,
89 key_binding_position: KeybindingPosition,
90 alpha: Option<f32>,
91 truncate: bool,
92 loading: bool,
93}
94
95impl Button {
96 /// Creates a new [`Button`] with a specified identifier and label.
97 ///
98 /// This is the primary constructor for a [`Button`] component. It initializes
99 /// the button with the provided identifier and label text, setting all other
100 /// properties to their default values, which can be customized using the
101 /// builder pattern methods provided by this struct.
102 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
103 Self {
104 base: ButtonLike::new(id),
105 label: label.into(),
106 label_color: None,
107 label_size: None,
108 selected_label: None,
109 selected_label_color: None,
110 start_icon: None,
111 end_icon: None,
112 key_binding: None,
113 key_binding_position: KeybindingPosition::default(),
114 alpha: None,
115 truncate: false,
116 loading: false,
117 }
118 }
119
120 /// Sets the color of the button's label.
121 pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
122 self.label_color = label_color.into();
123 self
124 }
125
126 /// Defines the size of the button's label.
127 pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
128 self.label_size = label_size.into();
129 self
130 }
131
132 /// Sets the label used when the button is in a selected state.
133 pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
134 self.selected_label = label.into().map(Into::into);
135 self
136 }
137
138 /// Sets the label color used when the button is in a selected state.
139 pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
140 self.selected_label_color = color.into();
141 self
142 }
143
144 /// Sets an icon to display at the start (left) of the button label.
145 ///
146 /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
147 pub fn start_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
148 self.start_icon = icon.into();
149 self
150 }
151
152 /// Sets an icon to display at the end (right) of the button label.
153 ///
154 /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
155 pub fn end_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
156 self.end_icon = icon.into();
157 self
158 }
159
160 /// Display the keybinding that triggers the button action.
161 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
162 self.key_binding = key_binding.into();
163 self
164 }
165
166 /// Sets the position of the keybinding relative to the button label.
167 ///
168 /// This method allows you to specify where the keybinding should be displayed
169 /// in relation to the button's label.
170 pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
171 self.key_binding_position = position;
172 self
173 }
174
175 /// Sets the alpha property of the color of label.
176 pub fn alpha(mut self, alpha: f32) -> Self {
177 self.alpha = Some(alpha);
178 self
179 }
180
181 /// Truncates overflowing labels with an ellipsis (`…`) if needed.
182 ///
183 /// Buttons with static labels should _never_ be truncated, ensure
184 /// this is only used when the label is dynamic and may overflow.
185 pub fn truncate(mut self, truncate: bool) -> Self {
186 self.truncate = truncate;
187 self
188 }
189
190 /// Displays a rotating loading spinner in place of the `start_icon`.
191 ///
192 /// When `loading` is `true`, any `start_icon` is ignored. and a rotating
193 pub fn loading(mut self, loading: bool) -> Self {
194 self.loading = loading;
195 self
196 }
197
198 /// Convenience for a Tailwind-style solid "primary" button: solid
199 /// `palette::primary(600)` background with white text.
200 ///
201 /// Equivalent to `.style(ButtonStyle::Tinted(TintColor::Accent))`.
202 pub fn primary(mut self) -> Self {
203 self.base = self.base.style(ButtonStyle::Tinted(TintColor::Accent));
204 self.label_color = Some(Color::Custom(gpui::white()));
205 self
206 }
207
208 /// Convenience for a Tailwind-style solid "danger" button: solid
209 /// `palette::danger(600)` background with white text.
210 ///
211 /// Equivalent to `.style(ButtonStyle::Tinted(TintColor::Error))`.
212 pub fn danger(mut self) -> Self {
213 self.base = self.base.style(ButtonStyle::Tinted(TintColor::Error));
214 self.label_color = Some(Color::Custom(gpui::white()));
215 self
216 }
217
218 /// Convenience for a Tailwind-style "soft" button: faint
219 /// `palette::primary(50)` background with `palette::primary(700)` text.
220 ///
221 /// Implemented as an additive background override on top of the
222 /// `Tinted(Accent)` style, so it does not require a new `ButtonStyle`
223 /// variant.
224 pub fn soft(mut self) -> Self {
225 self.base = self
226 .base
227 .style(ButtonStyle::Tinted(TintColor::Accent))
228 .background_override(palette::primary(50));
229 self.label_color = Some(Color::Custom(palette::primary(700)));
230 self
231 }
232
233 /// shadcn/ui variant alias (recommended vocabulary).
234 ///
235 /// Maps to existing [`ButtonStyle`] / convenience builders without
236 /// renaming legacy `.primary()` / `.danger()` / `.soft()` call sites.
237 /// For link-style buttons use [`ButtonLink`] instead.
238 pub fn variant(mut self, variant: ButtonVariant) -> Self {
239 match variant {
240 ButtonVariant::Default => self.primary(),
241 ButtonVariant::Destructive => self.danger(),
242 ButtonVariant::Outline => {
243 self.base = self.base.style(ButtonStyle::Outlined);
244 self.label_color = None;
245 self
246 }
247 ButtonVariant::Secondary => {
248 self.base = self.base.style(ButtonStyle::Filled);
249 self.label_color = None;
250 self
251 }
252 ButtonVariant::Ghost => {
253 self.base = self.base.style(ButtonStyle::Transparent);
254 self.label_color = None;
255 self
256 }
257 }
258 }
259
260 /// shadcn/ui size alias. Icon-only sizing uses [`IconButton`], not this API.
261 pub fn shadcn_size(mut self, size: ButtonSizeAlias) -> Self {
262 let mapped = match size {
263 ButtonSizeAlias::Sm => ButtonSize::Compact,
264 ButtonSizeAlias::Default => ButtonSize::Default,
265 ButtonSizeAlias::Lg => ButtonSize::Medium,
266 ButtonSizeAlias::Icon => ButtonSize::None,
267 };
268 self.base = self.base.size(mapped);
269 self
270 }
271}
272
273/// shadcn/ui button variants (additive alias over [`ButtonStyle`]).
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
275pub enum ButtonVariant {
276 #[default]
277 Default,
278 Destructive,
279 Outline,
280 Secondary,
281 Ghost,
282}
283
284/// shadcn/ui button sizes (additive alias over [`ButtonSize`]).
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
286pub enum ButtonSizeAlias {
287 Sm,
288 #[default]
289 Default,
290 Lg,
291 Icon,
292}
293
294impl Toggleable for Button {
295 /// Sets the selected state of the button.
296 ///
297 /// # Examples
298 ///
299 /// Create a toggleable button that changes appearance when selected:
300 ///
301 /// ```
302 /// use ui::prelude::*;
303 /// use ui::TintColor;
304 ///
305 /// let selected = true;
306 ///
307 /// Button::new("toggle_button", "Toggle Me")
308 /// .start_icon(Icon::new(IconName::Check))
309 /// .toggle_state(selected)
310 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
311 /// .on_click(|event, window, cx| {
312 /// // Toggle the selected state
313 /// });
314 /// ```
315 fn toggle_state(mut self, selected: bool) -> Self {
316 self.base = self.base.toggle_state(selected);
317 self
318 }
319}
320
321impl SelectableButton for Button {
322 /// Sets the style for the button in a selected state.
323 ///
324 /// # Examples
325 ///
326 /// Customize the selected appearance of a button:
327 ///
328 /// ```
329 /// use ui::prelude::*;
330 /// use ui::TintColor;
331 ///
332 /// Button::new("styled_button", "Styled Button")
333 /// .toggle_state(true)
334 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent));
335 /// ```
336 fn selected_style(mut self, style: ButtonStyle) -> Self {
337 self.base = self.base.selected_style(style);
338 self
339 }
340}
341
342impl Disableable for Button {
343 /// Disables the button, preventing interaction and changing its appearance.
344 ///
345 /// When disabled, the button's icon and label will use `Color::Disabled`.
346 ///
347 /// # Examples
348 ///
349 /// Create a disabled button:
350 ///
351 /// ```
352 /// use ui::prelude::*;
353 ///
354 /// Button::new("disabled_button", "Can't Click Me")
355 /// .disabled(true);
356 /// ```
357 fn disabled(mut self, disabled: bool) -> Self {
358 self.base = self.base.disabled(disabled);
359 self
360 }
361}
362
363impl Clickable for Button {
364 fn on_click(
365 mut self,
366 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
367 ) -> Self {
368 self.base = self.base.on_click(handler);
369 self
370 }
371
372 fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
373 self.base = self.base.cursor_style(cursor_style);
374 self
375 }
376}
377
378impl FixedWidth for Button {
379 /// Sets a fixed width for the button.
380 ///
381 /// # Examples
382 ///
383 /// Create a button with a fixed width of 100 pixels:
384 ///
385 /// ```
386 /// use ui::prelude::*;
387 ///
388 /// Button::new("fixed_width_button", "Fixed Width")
389 /// .width(px(100.0));
390 /// ```
391 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
392 self.base = self.base.width(width);
393 self
394 }
395
396 /// Makes the button take up the full width of its container.
397 ///
398 /// # Examples
399 ///
400 /// Create a button that takes up the full width of its container:
401 ///
402 /// ```
403 /// use ui::prelude::*;
404 ///
405 /// Button::new("full_width_button", "Full Width")
406 /// .full_width();
407 /// ```
408 fn full_width(mut self) -> Self {
409 self.base = self.base.full_width();
410 self
411 }
412}
413
414impl ButtonCommon for Button {
415 fn id(&self) -> &ElementId {
416 self.base.id()
417 }
418
419 /// Sets the visual style of the button.
420 fn style(mut self, style: ButtonStyle) -> Self {
421 self.base = self.base.style(style);
422 self
423 }
424
425 /// Sets the size of the button.
426 fn size(mut self, size: ButtonSize) -> Self {
427 self.base = self.base.size(size);
428 self
429 }
430
431 /// Sets a tooltip that appears on hover.
432 ///
433 /// # Examples
434 ///
435 /// Add a tooltip to a button:
436 ///
437 /// ```
438 /// use ui::{Tooltip, prelude::*};
439 ///
440 /// Button::new("tooltip_button", "Hover Me")
441 /// .tooltip(Tooltip::text("This is a tooltip"));
442 /// ```
443 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
444 self.base = self.base.tooltip(tooltip);
445 self
446 }
447
448 fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
449 self.base = self.base.tab_index(tab_index);
450 self
451 }
452
453 fn layer(mut self, elevation: ElevationIndex) -> Self {
454 self.base = self.base.layer(elevation);
455 self
456 }
457
458 fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
459 self.base = self.base.track_focus(focus_handle);
460 self
461 }
462}
463
464impl RenderOnce for Button {
465 #[allow(refining_impl_trait)]
466 fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
467 let is_disabled = self.base.disabled;
468 let is_selected = self.base.selected;
469 let button_style = self.base.style;
470
471 let label = self
472 .selected_label
473 .filter(|_| is_selected)
474 .unwrap_or(self.label);
475
476 let label_color = if is_disabled {
477 Color::Disabled
478 } else if is_selected {
479 self.selected_label_color.unwrap_or(Color::Selected)
480 } else if let Some(color) = self.label_color {
481 color
482 } else if matches!(button_style, ButtonStyle::Tinted(_)) {
483 // Solid Tinted backgrounds (primary/danger/etc.) need light text
484 // for contrast — applies to every `.style(Tinted(..))` caller, not
485 // just the `.primary()`/`.danger()` sugar.
486 Color::Custom(gpui::white())
487 } else {
488 Color::default()
489 };
490
491 self.base.child(
492 h_flex()
493 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
494 .gap(DynamicSpacing::Base04.rems(cx))
495 .when(self.loading, |this| {
496 this.child(
497 Icon::new(IconName::LoadCircle)
498 .size(IconSize::Small)
499 .color(Color::Muted)
500 .with_rotate_animation(2),
501 )
502 })
503 .when(!self.loading, |this| {
504 this.when_some(self.start_icon, |this, icon| {
505 this.child(if is_disabled {
506 icon.color(Color::Disabled)
507 } else if matches!(button_style, ButtonStyle::Tinted(_)) {
508 icon.color(Color::Custom(gpui::white()))
509 } else {
510 icon
511 })
512 })
513 })
514 .child(
515 h_flex()
516 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
517 .when(
518 self.key_binding_position == KeybindingPosition::Start,
519 |this| this.flex_row_reverse(),
520 )
521 .gap(DynamicSpacing::Base06.rems(cx))
522 .justify_between()
523 .child(
524 Label::new(label)
525 .color(label_color)
526 .size(self.label_size.unwrap_or_default())
527 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
528 .when(self.truncate, |this| this.truncate()),
529 )
530 .children(self.key_binding),
531 )
532 .when_some(self.end_icon, |this, icon| {
533 this.child(if is_disabled {
534 icon.color(Color::Disabled)
535 } else {
536 icon
537 })
538 }),
539 )
540 }
541}
542
543impl Component for Button {
544 fn scope() -> ComponentScope {
545 ComponentScope::Input
546 }
547
548 fn sort_name() -> &'static str {
549 "ButtonA"
550 }
551
552 fn description() -> Option<&'static str> {
553 Some("A button triggers an event or action.")
554 }
555
556 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
557 Some(
558 v_flex()
559 .gap_6()
560 .children(vec![
561 example_group_with_title(
562 "Button Styles",
563 vec![
564 single_example(
565 "Default",
566 Button::new("default", "Default").into_any_element(),
567 ),
568 single_example(
569 "Filled",
570 Button::new("filled", "Filled")
571 .style(ButtonStyle::Filled)
572 .into_any_element(),
573 ),
574 single_example(
575 "Subtle",
576 Button::new("outline", "Subtle")
577 .style(ButtonStyle::Subtle)
578 .into_any_element(),
579 ),
580 single_example(
581 "Tinted",
582 Button::new("tinted_accent_style", "Accent")
583 .style(ButtonStyle::Tinted(TintColor::Accent))
584 .into_any_element(),
585 ),
586 single_example(
587 "Transparent",
588 Button::new("transparent", "Transparent")
589 .style(ButtonStyle::Transparent)
590 .into_any_element(),
591 ),
592 ],
593 ),
594 example_group_with_title(
595 "Primary / Danger / Soft",
596 vec![
597 single_example(
598 "Primary",
599 Button::new("primary", "Primary")
600 .primary()
601 .into_any_element(),
602 ),
603 single_example(
604 "Danger",
605 Button::new("danger", "Danger").danger().into_any_element(),
606 ),
607 single_example(
608 "Soft",
609 Button::new("soft", "Soft").soft().into_any_element(),
610 ),
611 ],
612 ),
613 example_group_with_title(
614 "Variant \u{d7} Size Matrix",
615 vec![
616 single_example(
617 "Large",
618 h_flex()
619 .gap_2()
620 .child(
621 Button::new("matrix_primary_lg", "Primary")
622 .primary()
623 .size(ButtonSize::Large),
624 )
625 .child(
626 Button::new("matrix_danger_lg", "Danger")
627 .danger()
628 .size(ButtonSize::Large),
629 )
630 .child(
631 Button::new("matrix_soft_lg", "Soft")
632 .soft()
633 .size(ButtonSize::Large),
634 )
635 .into_any_element(),
636 ),
637 single_example(
638 "Medium",
639 h_flex()
640 .gap_2()
641 .child(
642 Button::new("matrix_primary_md", "Primary")
643 .primary()
644 .size(ButtonSize::Medium),
645 )
646 .child(
647 Button::new("matrix_danger_md", "Danger")
648 .danger()
649 .size(ButtonSize::Medium),
650 )
651 .child(
652 Button::new("matrix_soft_md", "Soft")
653 .soft()
654 .size(ButtonSize::Medium),
655 )
656 .into_any_element(),
657 ),
658 single_example(
659 "Compact",
660 h_flex()
661 .gap_2()
662 .child(
663 Button::new("matrix_primary_cp", "Primary")
664 .primary()
665 .size(ButtonSize::Compact),
666 )
667 .child(
668 Button::new("matrix_danger_cp", "Danger")
669 .danger()
670 .size(ButtonSize::Compact),
671 )
672 .child(
673 Button::new("matrix_soft_cp", "Soft")
674 .soft()
675 .size(ButtonSize::Compact),
676 )
677 .into_any_element(),
678 ),
679 ],
680 ),
681 example_group_with_title(
682 "Tint Styles",
683 vec![
684 single_example(
685 "Accent",
686 Button::new("tinted_accent", "Accent")
687 .style(ButtonStyle::Tinted(TintColor::Accent))
688 .into_any_element(),
689 ),
690 single_example(
691 "Error",
692 Button::new("tinted_negative", "Error")
693 .style(ButtonStyle::Tinted(TintColor::Error))
694 .into_any_element(),
695 ),
696 single_example(
697 "Warning",
698 Button::new("tinted_warning", "Warning")
699 .style(ButtonStyle::Tinted(TintColor::Warning))
700 .into_any_element(),
701 ),
702 single_example(
703 "Success",
704 Button::new("tinted_positive", "Success")
705 .style(ButtonStyle::Tinted(TintColor::Success))
706 .into_any_element(),
707 ),
708 ],
709 ),
710 example_group_with_title(
711 "Special States",
712 vec![
713 single_example(
714 "Default",
715 Button::new("default_state", "Default").into_any_element(),
716 ),
717 single_example(
718 "Disabled",
719 Button::new("disabled", "Disabled")
720 .disabled(true)
721 .into_any_element(),
722 ),
723 single_example(
724 "Selected",
725 Button::new("selected", "Selected")
726 .toggle_state(true)
727 .into_any_element(),
728 ),
729 ],
730 ),
731 example_group_with_title(
732 "Buttons with Icons",
733 vec![
734 single_example(
735 "Start Icon",
736 Button::new("icon_start", "Start Icon")
737 .start_icon(Icon::new(IconName::Check))
738 .into_any_element(),
739 ),
740 single_example(
741 "End Icon",
742 Button::new("icon_end", "End Icon")
743 .end_icon(Icon::new(IconName::Check))
744 .into_any_element(),
745 ),
746 single_example(
747 "Both Icons",
748 Button::new("both_icons", "Both Icons")
749 .start_icon(Icon::new(IconName::Check))
750 .end_icon(Icon::new(IconName::ChevronDown))
751 .into_any_element(),
752 ),
753 single_example(
754 "Icon Color",
755 Button::new("icon_color", "Icon Color")
756 .start_icon(Icon::new(IconName::Check).color(Color::Accent))
757 .into_any_element(),
758 ),
759 ],
760 ),
761 ])
762 .into_any_element(),
763 )
764 }
765}