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
199impl Toggleable for Button {
200    /// Sets the selected state of the button.
201    ///
202    /// # Examples
203    ///
204    /// Create a toggleable button that changes appearance when selected:
205    ///
206    /// ```
207    /// use ui::prelude::*;
208    /// use ui::TintColor;
209    ///
210    /// let selected = true;
211    ///
212    /// Button::new("toggle_button", "Toggle Me")
213    ///     .start_icon(Icon::new(IconName::Check))
214    ///     .toggle_state(selected)
215    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
216    ///     .on_click(|event, window, cx| {
217    ///         // Toggle the selected state
218    ///     });
219    /// ```
220    fn toggle_state(mut self, selected: bool) -> Self {
221        self.base = self.base.toggle_state(selected);
222        self
223    }
224}
225
226impl SelectableButton for Button {
227    /// Sets the style for the button in a selected state.
228    ///
229    /// # Examples
230    ///
231    /// Customize the selected appearance of a button:
232    ///
233    /// ```
234    /// use ui::prelude::*;
235    /// use ui::TintColor;
236    ///
237    /// Button::new("styled_button", "Styled Button")
238    ///     .toggle_state(true)
239    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent));
240    /// ```
241    fn selected_style(mut self, style: ButtonStyle) -> Self {
242        self.base = self.base.selected_style(style);
243        self
244    }
245}
246
247impl Disableable for Button {
248    /// Disables the button, preventing interaction and changing its appearance.
249    ///
250    /// When disabled, the button's icon and label will use `Color::Disabled`.
251    ///
252    /// # Examples
253    ///
254    /// Create a disabled button:
255    ///
256    /// ```
257    /// use ui::prelude::*;
258    ///
259    /// Button::new("disabled_button", "Can't Click Me")
260    ///     .disabled(true);
261    /// ```
262    fn disabled(mut self, disabled: bool) -> Self {
263        self.base = self.base.disabled(disabled);
264        self
265    }
266}
267
268impl Clickable for Button {
269    fn on_click(
270        mut self,
271        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
272    ) -> Self {
273        self.base = self.base.on_click(handler);
274        self
275    }
276
277    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
278        self.base = self.base.cursor_style(cursor_style);
279        self
280    }
281}
282
283impl FixedWidth for Button {
284    /// Sets a fixed width for the button.
285    ///
286    /// # Examples
287    ///
288    /// Create a button with a fixed width of 100 pixels:
289    ///
290    /// ```
291    /// use ui::prelude::*;
292    ///
293    /// Button::new("fixed_width_button", "Fixed Width")
294    ///     .width(px(100.0));
295    /// ```
296    fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
297        self.base = self.base.width(width);
298        self
299    }
300
301    /// Makes the button take up the full width of its container.
302    ///
303    /// # Examples
304    ///
305    /// Create a button that takes up the full width of its container:
306    ///
307    /// ```
308    /// use ui::prelude::*;
309    ///
310    /// Button::new("full_width_button", "Full Width")
311    ///     .full_width();
312    /// ```
313    fn full_width(mut self) -> Self {
314        self.base = self.base.full_width();
315        self
316    }
317}
318
319impl ButtonCommon for Button {
320    fn id(&self) -> &ElementId {
321        self.base.id()
322    }
323
324    /// Sets the visual style of the button.
325    fn style(mut self, style: ButtonStyle) -> Self {
326        self.base = self.base.style(style);
327        self
328    }
329
330    /// Sets the size of the button.
331    fn size(mut self, size: ButtonSize) -> Self {
332        self.base = self.base.size(size);
333        self
334    }
335
336    /// Sets a tooltip that appears on hover.
337    ///
338    /// # Examples
339    ///
340    /// Add a tooltip to a button:
341    ///
342    /// ```
343    /// use ui::{Tooltip, prelude::*};
344    ///
345    /// Button::new("tooltip_button", "Hover Me")
346    ///     .tooltip(Tooltip::text("This is a tooltip"));
347    /// ```
348    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
349        self.base = self.base.tooltip(tooltip);
350        self
351    }
352
353    fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
354        self.base = self.base.tab_index(tab_index);
355        self
356    }
357
358    fn layer(mut self, elevation: ElevationIndex) -> Self {
359        self.base = self.base.layer(elevation);
360        self
361    }
362
363    fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
364        self.base = self.base.track_focus(focus_handle);
365        self
366    }
367}
368
369impl RenderOnce for Button {
370    #[allow(refining_impl_trait)]
371    fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
372        let is_disabled = self.base.disabled;
373        let is_selected = self.base.selected;
374
375        let label = self
376            .selected_label
377            .filter(|_| is_selected)
378            .unwrap_or(self.label);
379
380        let label_color = if is_disabled {
381            Color::Disabled
382        } else if is_selected {
383            self.selected_label_color.unwrap_or(Color::Selected)
384        } else {
385            self.label_color.unwrap_or_default()
386        };
387
388        self.base.child(
389            h_flex()
390                .when(self.truncate, |this| this.min_w_0().overflow_hidden())
391                .gap(DynamicSpacing::Base04.rems(cx))
392                .when(self.loading, |this| {
393                    this.child(
394                        Icon::new(IconName::LoadCircle)
395                            .size(IconSize::Small)
396                            .color(Color::Muted)
397                            .with_rotate_animation(2),
398                    )
399                })
400                .when(!self.loading, |this| {
401                    this.when_some(self.start_icon, |this, icon| {
402                        this.child(if is_disabled {
403                            icon.color(Color::Disabled)
404                        } else {
405                            icon
406                        })
407                    })
408                })
409                .child(
410                    h_flex()
411                        .when(self.truncate, |this| this.min_w_0().overflow_hidden())
412                        .when(
413                            self.key_binding_position == KeybindingPosition::Start,
414                            |this| this.flex_row_reverse(),
415                        )
416                        .gap(DynamicSpacing::Base06.rems(cx))
417                        .justify_between()
418                        .child(
419                            Label::new(label)
420                                .color(label_color)
421                                .size(self.label_size.unwrap_or_default())
422                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
423                                .when(self.truncate, |this| this.truncate()),
424                        )
425                        .children(self.key_binding),
426                )
427                .when_some(self.end_icon, |this, icon| {
428                    this.child(if is_disabled {
429                        icon.color(Color::Disabled)
430                    } else {
431                        icon
432                    })
433                }),
434        )
435    }
436}
437
438impl Component for Button {
439    fn scope() -> ComponentScope {
440        ComponentScope::Input
441    }
442
443    fn sort_name() -> &'static str {
444        "ButtonA"
445    }
446
447    fn description() -> Option<&'static str> {
448        Some("A button triggers an event or action.")
449    }
450
451    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
452        Some(
453            v_flex()
454                .gap_6()
455                .children(vec![
456                    example_group_with_title(
457                        "Button Styles",
458                        vec![
459                            single_example(
460                                "Default",
461                                Button::new("default", "Default").into_any_element(),
462                            ),
463                            single_example(
464                                "Filled",
465                                Button::new("filled", "Filled")
466                                    .style(ButtonStyle::Filled)
467                                    .into_any_element(),
468                            ),
469                            single_example(
470                                "Subtle",
471                                Button::new("outline", "Subtle")
472                                    .style(ButtonStyle::Subtle)
473                                    .into_any_element(),
474                            ),
475                            single_example(
476                                "Tinted",
477                                Button::new("tinted_accent_style", "Accent")
478                                    .style(ButtonStyle::Tinted(TintColor::Accent))
479                                    .into_any_element(),
480                            ),
481                            single_example(
482                                "Transparent",
483                                Button::new("transparent", "Transparent")
484                                    .style(ButtonStyle::Transparent)
485                                    .into_any_element(),
486                            ),
487                        ],
488                    ),
489                    example_group_with_title(
490                        "Tint Styles",
491                        vec![
492                            single_example(
493                                "Accent",
494                                Button::new("tinted_accent", "Accent")
495                                    .style(ButtonStyle::Tinted(TintColor::Accent))
496                                    .into_any_element(),
497                            ),
498                            single_example(
499                                "Error",
500                                Button::new("tinted_negative", "Error")
501                                    .style(ButtonStyle::Tinted(TintColor::Error))
502                                    .into_any_element(),
503                            ),
504                            single_example(
505                                "Warning",
506                                Button::new("tinted_warning", "Warning")
507                                    .style(ButtonStyle::Tinted(TintColor::Warning))
508                                    .into_any_element(),
509                            ),
510                            single_example(
511                                "Success",
512                                Button::new("tinted_positive", "Success")
513                                    .style(ButtonStyle::Tinted(TintColor::Success))
514                                    .into_any_element(),
515                            ),
516                        ],
517                    ),
518                    example_group_with_title(
519                        "Special States",
520                        vec![
521                            single_example(
522                                "Default",
523                                Button::new("default_state", "Default").into_any_element(),
524                            ),
525                            single_example(
526                                "Disabled",
527                                Button::new("disabled", "Disabled")
528                                    .disabled(true)
529                                    .into_any_element(),
530                            ),
531                            single_example(
532                                "Selected",
533                                Button::new("selected", "Selected")
534                                    .toggle_state(true)
535                                    .into_any_element(),
536                            ),
537                        ],
538                    ),
539                    example_group_with_title(
540                        "Buttons with Icons",
541                        vec![
542                            single_example(
543                                "Start Icon",
544                                Button::new("icon_start", "Start Icon")
545                                    .start_icon(Icon::new(IconName::Check))
546                                    .into_any_element(),
547                            ),
548                            single_example(
549                                "End Icon",
550                                Button::new("icon_end", "End Icon")
551                                    .end_icon(Icon::new(IconName::Check))
552                                    .into_any_element(),
553                            ),
554                            single_example(
555                                "Both Icons",
556                                Button::new("both_icons", "Both Icons")
557                                    .start_icon(Icon::new(IconName::Check))
558                                    .end_icon(Icon::new(IconName::ChevronDown))
559                                    .into_any_element(),
560                            ),
561                            single_example(
562                                "Icon Color",
563                                Button::new("icon_color", "Icon Color")
564                                    .start_icon(Icon::new(IconName::Check).color(Color::Accent))
565                                    .into_any_element(),
566                            ),
567                        ],
568                    ),
569                ])
570                .into_any_element(),
571        )
572    }
573}