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
234impl Toggleable for Button {
235 /// Sets the selected state of the button.
236 ///
237 /// # Examples
238 ///
239 /// Create a toggleable button that changes appearance when selected:
240 ///
241 /// ```
242 /// use ui::prelude::*;
243 /// use ui::TintColor;
244 ///
245 /// let selected = true;
246 ///
247 /// Button::new("toggle_button", "Toggle Me")
248 /// .start_icon(Icon::new(IconName::Check))
249 /// .toggle_state(selected)
250 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
251 /// .on_click(|event, window, cx| {
252 /// // Toggle the selected state
253 /// });
254 /// ```
255 fn toggle_state(mut self, selected: bool) -> Self {
256 self.base = self.base.toggle_state(selected);
257 self
258 }
259}
260
261impl SelectableButton for Button {
262 /// Sets the style for the button in a selected state.
263 ///
264 /// # Examples
265 ///
266 /// Customize the selected appearance of a button:
267 ///
268 /// ```
269 /// use ui::prelude::*;
270 /// use ui::TintColor;
271 ///
272 /// Button::new("styled_button", "Styled Button")
273 /// .toggle_state(true)
274 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent));
275 /// ```
276 fn selected_style(mut self, style: ButtonStyle) -> Self {
277 self.base = self.base.selected_style(style);
278 self
279 }
280}
281
282impl Disableable for Button {
283 /// Disables the button, preventing interaction and changing its appearance.
284 ///
285 /// When disabled, the button's icon and label will use `Color::Disabled`.
286 ///
287 /// # Examples
288 ///
289 /// Create a disabled button:
290 ///
291 /// ```
292 /// use ui::prelude::*;
293 ///
294 /// Button::new("disabled_button", "Can't Click Me")
295 /// .disabled(true);
296 /// ```
297 fn disabled(mut self, disabled: bool) -> Self {
298 self.base = self.base.disabled(disabled);
299 self
300 }
301}
302
303impl Clickable for Button {
304 fn on_click(
305 mut self,
306 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
307 ) -> Self {
308 self.base = self.base.on_click(handler);
309 self
310 }
311
312 fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
313 self.base = self.base.cursor_style(cursor_style);
314 self
315 }
316}
317
318impl FixedWidth for Button {
319 /// Sets a fixed width for the button.
320 ///
321 /// # Examples
322 ///
323 /// Create a button with a fixed width of 100 pixels:
324 ///
325 /// ```
326 /// use ui::prelude::*;
327 ///
328 /// Button::new("fixed_width_button", "Fixed Width")
329 /// .width(px(100.0));
330 /// ```
331 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
332 self.base = self.base.width(width);
333 self
334 }
335
336 /// Makes the button take up the full width of its container.
337 ///
338 /// # Examples
339 ///
340 /// Create a button that takes up the full width of its container:
341 ///
342 /// ```
343 /// use ui::prelude::*;
344 ///
345 /// Button::new("full_width_button", "Full Width")
346 /// .full_width();
347 /// ```
348 fn full_width(mut self) -> Self {
349 self.base = self.base.full_width();
350 self
351 }
352}
353
354impl ButtonCommon for Button {
355 fn id(&self) -> &ElementId {
356 self.base.id()
357 }
358
359 /// Sets the visual style of the button.
360 fn style(mut self, style: ButtonStyle) -> Self {
361 self.base = self.base.style(style);
362 self
363 }
364
365 /// Sets the size of the button.
366 fn size(mut self, size: ButtonSize) -> Self {
367 self.base = self.base.size(size);
368 self
369 }
370
371 /// Sets a tooltip that appears on hover.
372 ///
373 /// # Examples
374 ///
375 /// Add a tooltip to a button:
376 ///
377 /// ```
378 /// use ui::{Tooltip, prelude::*};
379 ///
380 /// Button::new("tooltip_button", "Hover Me")
381 /// .tooltip(Tooltip::text("This is a tooltip"));
382 /// ```
383 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
384 self.base = self.base.tooltip(tooltip);
385 self
386 }
387
388 fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
389 self.base = self.base.tab_index(tab_index);
390 self
391 }
392
393 fn layer(mut self, elevation: ElevationIndex) -> Self {
394 self.base = self.base.layer(elevation);
395 self
396 }
397
398 fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
399 self.base = self.base.track_focus(focus_handle);
400 self
401 }
402}
403
404impl RenderOnce for Button {
405 #[allow(refining_impl_trait)]
406 fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
407 let is_disabled = self.base.disabled;
408 let is_selected = self.base.selected;
409 let button_style = self.base.style;
410
411 let label = self
412 .selected_label
413 .filter(|_| is_selected)
414 .unwrap_or(self.label);
415
416 let label_color = if is_disabled {
417 Color::Disabled
418 } else if is_selected {
419 self.selected_label_color.unwrap_or(Color::Selected)
420 } else if let Some(color) = self.label_color {
421 color
422 } else if matches!(button_style, ButtonStyle::Tinted(_)) {
423 // Solid Tinted backgrounds (primary/danger/etc.) need light text
424 // for contrast — applies to every `.style(Tinted(..))` caller, not
425 // just the `.primary()`/`.danger()` sugar.
426 Color::Custom(gpui::white())
427 } else {
428 Color::default()
429 };
430
431 self.base.child(
432 h_flex()
433 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
434 .gap(DynamicSpacing::Base04.rems(cx))
435 .when(self.loading, |this| {
436 this.child(
437 Icon::new(IconName::LoadCircle)
438 .size(IconSize::Small)
439 .color(Color::Muted)
440 .with_rotate_animation(2),
441 )
442 })
443 .when(!self.loading, |this| {
444 this.when_some(self.start_icon, |this, icon| {
445 this.child(if is_disabled {
446 icon.color(Color::Disabled)
447 } else if matches!(button_style, ButtonStyle::Tinted(_)) {
448 icon.color(Color::Custom(gpui::white()))
449 } else {
450 icon
451 })
452 })
453 })
454 .child(
455 h_flex()
456 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
457 .when(
458 self.key_binding_position == KeybindingPosition::Start,
459 |this| this.flex_row_reverse(),
460 )
461 .gap(DynamicSpacing::Base06.rems(cx))
462 .justify_between()
463 .child(
464 Label::new(label)
465 .color(label_color)
466 .size(self.label_size.unwrap_or_default())
467 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
468 .when(self.truncate, |this| this.truncate()),
469 )
470 .children(self.key_binding),
471 )
472 .when_some(self.end_icon, |this, icon| {
473 this.child(if is_disabled {
474 icon.color(Color::Disabled)
475 } else {
476 icon
477 })
478 }),
479 )
480 }
481}
482
483impl Component for Button {
484 fn scope() -> ComponentScope {
485 ComponentScope::Input
486 }
487
488 fn sort_name() -> &'static str {
489 "ButtonA"
490 }
491
492 fn description() -> Option<&'static str> {
493 Some("A button triggers an event or action.")
494 }
495
496 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
497 Some(
498 v_flex()
499 .gap_6()
500 .children(vec![
501 example_group_with_title(
502 "Button Styles",
503 vec![
504 single_example(
505 "Default",
506 Button::new("default", "Default").into_any_element(),
507 ),
508 single_example(
509 "Filled",
510 Button::new("filled", "Filled")
511 .style(ButtonStyle::Filled)
512 .into_any_element(),
513 ),
514 single_example(
515 "Subtle",
516 Button::new("outline", "Subtle")
517 .style(ButtonStyle::Subtle)
518 .into_any_element(),
519 ),
520 single_example(
521 "Tinted",
522 Button::new("tinted_accent_style", "Accent")
523 .style(ButtonStyle::Tinted(TintColor::Accent))
524 .into_any_element(),
525 ),
526 single_example(
527 "Transparent",
528 Button::new("transparent", "Transparent")
529 .style(ButtonStyle::Transparent)
530 .into_any_element(),
531 ),
532 ],
533 ),
534 example_group_with_title(
535 "Primary / Danger / Soft",
536 vec![
537 single_example(
538 "Primary",
539 Button::new("primary", "Primary")
540 .primary()
541 .into_any_element(),
542 ),
543 single_example(
544 "Danger",
545 Button::new("danger", "Danger").danger().into_any_element(),
546 ),
547 single_example(
548 "Soft",
549 Button::new("soft", "Soft").soft().into_any_element(),
550 ),
551 ],
552 ),
553 example_group_with_title(
554 "Variant \u{d7} Size Matrix",
555 vec![
556 single_example(
557 "Large",
558 h_flex()
559 .gap_2()
560 .child(
561 Button::new("matrix_primary_lg", "Primary")
562 .primary()
563 .size(ButtonSize::Large),
564 )
565 .child(
566 Button::new("matrix_danger_lg", "Danger")
567 .danger()
568 .size(ButtonSize::Large),
569 )
570 .child(
571 Button::new("matrix_soft_lg", "Soft")
572 .soft()
573 .size(ButtonSize::Large),
574 )
575 .into_any_element(),
576 ),
577 single_example(
578 "Medium",
579 h_flex()
580 .gap_2()
581 .child(
582 Button::new("matrix_primary_md", "Primary")
583 .primary()
584 .size(ButtonSize::Medium),
585 )
586 .child(
587 Button::new("matrix_danger_md", "Danger")
588 .danger()
589 .size(ButtonSize::Medium),
590 )
591 .child(
592 Button::new("matrix_soft_md", "Soft")
593 .soft()
594 .size(ButtonSize::Medium),
595 )
596 .into_any_element(),
597 ),
598 single_example(
599 "Compact",
600 h_flex()
601 .gap_2()
602 .child(
603 Button::new("matrix_primary_cp", "Primary")
604 .primary()
605 .size(ButtonSize::Compact),
606 )
607 .child(
608 Button::new("matrix_danger_cp", "Danger")
609 .danger()
610 .size(ButtonSize::Compact),
611 )
612 .child(
613 Button::new("matrix_soft_cp", "Soft")
614 .soft()
615 .size(ButtonSize::Compact),
616 )
617 .into_any_element(),
618 ),
619 ],
620 ),
621 example_group_with_title(
622 "Tint Styles",
623 vec![
624 single_example(
625 "Accent",
626 Button::new("tinted_accent", "Accent")
627 .style(ButtonStyle::Tinted(TintColor::Accent))
628 .into_any_element(),
629 ),
630 single_example(
631 "Error",
632 Button::new("tinted_negative", "Error")
633 .style(ButtonStyle::Tinted(TintColor::Error))
634 .into_any_element(),
635 ),
636 single_example(
637 "Warning",
638 Button::new("tinted_warning", "Warning")
639 .style(ButtonStyle::Tinted(TintColor::Warning))
640 .into_any_element(),
641 ),
642 single_example(
643 "Success",
644 Button::new("tinted_positive", "Success")
645 .style(ButtonStyle::Tinted(TintColor::Success))
646 .into_any_element(),
647 ),
648 ],
649 ),
650 example_group_with_title(
651 "Special States",
652 vec![
653 single_example(
654 "Default",
655 Button::new("default_state", "Default").into_any_element(),
656 ),
657 single_example(
658 "Disabled",
659 Button::new("disabled", "Disabled")
660 .disabled(true)
661 .into_any_element(),
662 ),
663 single_example(
664 "Selected",
665 Button::new("selected", "Selected")
666 .toggle_state(true)
667 .into_any_element(),
668 ),
669 ],
670 ),
671 example_group_with_title(
672 "Buttons with Icons",
673 vec![
674 single_example(
675 "Start Icon",
676 Button::new("icon_start", "Start Icon")
677 .start_icon(Icon::new(IconName::Check))
678 .into_any_element(),
679 ),
680 single_example(
681 "End Icon",
682 Button::new("icon_end", "End Icon")
683 .end_icon(Icon::new(IconName::Check))
684 .into_any_element(),
685 ),
686 single_example(
687 "Both Icons",
688 Button::new("both_icons", "Both Icons")
689 .start_icon(Icon::new(IconName::Check))
690 .end_icon(Icon::new(IconName::ChevronDown))
691 .into_any_element(),
692 ),
693 single_example(
694 "Icon Color",
695 Button::new("icon_color", "Icon Color")
696 .start_icon(Icon::new(IconName::Check).color(Color::Accent))
697 .into_any_element(),
698 ),
699 ],
700 ),
701 ])
702 .into_any_element(),
703 )
704 }
705}