button.rs

  1use component::{ComponentPreview, example_group_with_title, single_example};
  2use gpui::{AnyElement, AnyView, DefiniteLength};
  3use ui_macros::IntoComponent;
  4
  5use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label};
  6use crate::{
  7    Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, KeybindingPosition, TintColor,
  8    prelude::*,
  9};
 10
 11use super::button_icon::ButtonIcon;
 12
 13/// An element that creates a button with a label and an optional icon.
 14///
 15/// Common buttons:
 16/// - Label, Icon + Label: [`Button`] (this component)
 17/// - Icon only: [`IconButton`]
 18/// - Custom: [`ButtonLike`]
 19///
 20/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
 21/// [`ButtonLike`] directly.
 22///
 23/// # Examples
 24///
 25/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
 26/// indicates what action will be performed when the button is clicked.
 27///
 28/// ```
 29/// use ui::prelude::*;
 30///
 31/// Button::new("button_id", "Click me!")
 32///     .on_click(|event, window, cx| {
 33///         // Handle click event
 34///     });
 35/// ```
 36///
 37/// **A toggleable button**, is typically used in scenarios such as a toolbar,
 38/// where the button's state indicates whether a feature is enabled or not, or
 39/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
 40///
 41/// ```
 42/// use ui::prelude::*;
 43///
 44/// Button::new("button_id", "Click me!")
 45///     .icon(IconName::Check)
 46///     .selected(true)
 47///     .on_click(|event, window, cx| {
 48///         // Handle click event
 49///     });
 50/// ```
 51///
 52/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
 53///
 54/// ```
 55/// use ui::prelude::*;
 56/// use ui::TintColor;
 57///
 58/// Button::new("button_id", "Click me!")
 59///     .selected(true)
 60///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 61///     .on_click(|event, window, cx| {
 62///         // Handle click event
 63///     });
 64/// ```
 65/// This will create a button with a blue tinted background when selected.
 66///
 67/// **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.
 68/// The button's content, including text and icons, is centered by default.
 69///
 70/// ```
 71/// use ui::prelude::*;
 72///
 73/// let button = Button::new("button_id", "Click me!")
 74///     .full_width()
 75///     .on_click(|event, window, cx| {
 76///         // Handle click event
 77///     });
 78/// ```
 79///
 80#[derive(IntoElement, IntoComponent)]
 81#[component(scope = "Input")]
 82pub struct Button {
 83    base: ButtonLike,
 84    label: SharedString,
 85    label_color: Option<Color>,
 86    label_size: Option<LabelSize>,
 87    selected_label: Option<SharedString>,
 88    selected_label_color: Option<Color>,
 89    icon: Option<IconName>,
 90    icon_position: Option<IconPosition>,
 91    icon_size: Option<IconSize>,
 92    icon_color: Option<Color>,
 93    selected_icon: Option<IconName>,
 94    selected_icon_color: Option<Color>,
 95    key_binding: Option<KeyBinding>,
 96    key_binding_position: KeybindingPosition,
 97    alpha: Option<f32>,
 98    truncate: bool,
 99}
100
101impl Button {
102    /// Creates a new [`Button`] with a specified identifier and label.
103    ///
104    /// This is the primary constructor for a [`Button`] component. It initializes
105    /// the button with the provided identifier and label text, setting all other
106    /// properties to their default values, which can be customized using the
107    /// builder pattern methods provided by this struct.
108    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
109        Self {
110            base: ButtonLike::new(id),
111            label: label.into(),
112            label_color: None,
113            label_size: None,
114            selected_label: None,
115            selected_label_color: None,
116            icon: None,
117            icon_position: None,
118            icon_size: None,
119            icon_color: None,
120            selected_icon: None,
121            selected_icon_color: None,
122            key_binding: None,
123            key_binding_position: KeybindingPosition::default(),
124            alpha: None,
125            truncate: false,
126        }
127    }
128
129    /// Sets the color of the button's label.
130    pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
131        self.label_color = label_color.into();
132        self
133    }
134
135    /// Defines the size of the button's label.
136    pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
137        self.label_size = label_size.into();
138        self
139    }
140
141    /// Sets the label used when the button is in a selected state.
142    pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
143        self.selected_label = label.into().map(Into::into);
144        self
145    }
146
147    /// Sets the label color used when the button is in a selected state.
148    pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
149        self.selected_label_color = color.into();
150        self
151    }
152
153    /// Assigns an icon to the button.
154    pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
155        self.icon = icon.into();
156        self
157    }
158
159    /// Sets the position of the icon relative to the label.
160    pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
161        self.icon_position = icon_position.into();
162        self
163    }
164
165    /// Specifies the size of the button's icon.
166    pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
167        self.icon_size = icon_size.into();
168        self
169    }
170
171    /// Sets the color of the button's icon.
172    pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
173        self.icon_color = icon_color.into();
174        self
175    }
176
177    /// Chooses an icon to display when the button is in a selected state.
178    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
179        self.selected_icon = icon.into();
180        self
181    }
182
183    /// Sets the icon color used when the button is in a selected state.
184    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
185        self.selected_icon_color = color.into();
186        self
187    }
188
189    /// Display the keybinding that triggers the button action.
190    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
191        self.key_binding = key_binding.into();
192        self
193    }
194
195    /// Sets the position of the keybinding relative to the button label.
196    ///
197    /// This method allows you to specify where the keybinding should be displayed
198    /// in relation to the button's label.
199    pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
200        self.key_binding_position = position;
201        self
202    }
203
204    /// Sets the alpha property of the color of label.
205    pub fn alpha(mut self, alpha: f32) -> Self {
206        self.alpha = Some(alpha);
207        self
208    }
209
210    /// Truncates overflowing labels with an ellipsis (`…`) if needed.
211    ///
212    /// Buttons with static labels should _never_ be truncated, ensure
213    /// this is only used when the label is dynamic and may overflow.
214    pub fn truncate(mut self, truncate: bool) -> Self {
215        self.truncate = truncate;
216        self
217    }
218}
219
220impl Toggleable for Button {
221    /// Sets the selected state of the button.
222    ///
223    /// This method allows the selection state of the button to be specified.
224    /// It modifies the button's appearance to reflect its selected state.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use ui::prelude::*;
230    ///
231    /// Button::new("button_id", "Click me!")
232    ///     .selected(true)
233    ///     .on_click(|event, window, cx| {
234    ///         // Handle click event
235    ///     });
236    /// ```
237    ///
238    /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
239    fn toggle_state(mut self, selected: bool) -> Self {
240        self.base = self.base.toggle_state(selected);
241        self
242    }
243}
244
245impl SelectableButton for Button {
246    /// Sets the style for the button when selected.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use ui::prelude::*;
252    /// use ui::TintColor;
253    ///
254    /// Button::new("button_id", "Click me!")
255    ///     .selected(true)
256    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
257    ///     .on_click(|event, window, cx| {
258    ///         // Handle click event
259    ///     });
260    /// ```
261    /// This results in a button with a blue tinted background when selected.
262    fn selected_style(mut self, style: ButtonStyle) -> Self {
263        self.base = self.base.selected_style(style);
264        self
265    }
266}
267
268impl Disableable for Button {
269    /// Disables the button.
270    ///
271    /// This method allows the button to be disabled. When a button is disabled,
272    /// it doesn't react to user interactions and its appearance is updated to reflect this.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use ui::prelude::*;
278    ///
279    /// Button::new("button_id", "Click me!")
280    ///     .disabled(true)
281    ///     .on_click(|event, window, cx| {
282    ///         // Handle click event
283    ///     });
284    /// ```
285    ///
286    /// This results in a button that is disabled and does not respond to click events.
287    fn disabled(mut self, disabled: bool) -> Self {
288        self.base = self.base.disabled(disabled);
289        self
290    }
291}
292
293impl Clickable for Button {
294    /// Sets the click event handler for the button.
295    fn on_click(
296        mut self,
297        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
298    ) -> Self {
299        self.base = self.base.on_click(handler);
300        self
301    }
302
303    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
304        self.base = self.base.cursor_style(cursor_style);
305        self
306    }
307}
308
309impl FixedWidth for Button {
310    /// Sets a fixed width for the button.
311    ///
312    /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
313    /// Sets a fixed width for the button.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use ui::prelude::*;
319    ///
320    /// Button::new("button_id", "Click me!")
321    ///     .width(px(100.).into())
322    ///     .on_click(|event, window, cx| {
323    ///         // Handle click event
324    ///     });
325    /// ```
326    ///
327    /// This sets the button's width to be exactly 100 pixels.
328    fn width(mut self, width: DefiniteLength) -> Self {
329        self.base = self.base.width(width);
330        self
331    }
332
333    /// Sets the button to occupy the full width of its container.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use ui::prelude::*;
339    ///
340    /// Button::new("button_id", "Click me!")
341    ///     .full_width()
342    ///     .on_click(|event, window, cx| {
343    ///         // Handle click event
344    ///     });
345    /// ```
346    ///
347    /// This stretches the button to the full width of its container.
348    fn full_width(mut self) -> Self {
349        self.base = self.base.full_width();
350        self
351    }
352}
353
354impl ButtonCommon for Button {
355    /// Sets the button's id.
356    fn id(&self) -> &ElementId {
357        self.base.id()
358    }
359
360    /// Sets the visual style of the button using a [`ButtonStyle`].
361    fn style(mut self, style: ButtonStyle) -> Self {
362        self.base = self.base.style(style);
363        self
364    }
365
366    /// Sets the button's size using a [`ButtonSize`].
367    fn size(mut self, size: ButtonSize) -> Self {
368        self.base = self.base.size(size);
369        self
370    }
371
372    /// Sets a tooltip for the button.
373    ///
374    /// This method allows a tooltip to be set for the button. The tooltip is a function that
375    /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The
376    /// tooltip is displayed when the user hovers over the button.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use ui::prelude::*;
382    /// use ui::Tooltip;
383    ///
384    /// Button::new("button_id", "Click me!")
385    ///     .tooltip(Tooltip::text_f("This is a tooltip", cx))
386    ///     .on_click(|event, window, cx| {
387    ///         // Handle click event
388    ///     });
389    /// ```
390    ///
391    /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
392    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
393        self.base = self.base.tooltip(tooltip);
394        self
395    }
396
397    fn layer(mut self, elevation: ElevationIndex) -> Self {
398        self.base = self.base.layer(elevation);
399        self
400    }
401}
402
403impl RenderOnce for Button {
404    #[allow(refining_impl_trait)]
405    fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
406        let is_disabled = self.base.disabled;
407        let is_selected = self.base.selected;
408
409        let label = self
410            .selected_label
411            .filter(|_| is_selected)
412            .unwrap_or(self.label);
413
414        let label_color = if is_disabled {
415            Color::Disabled
416        } else if is_selected {
417            self.selected_label_color.unwrap_or(Color::Selected)
418        } else {
419            self.label_color.unwrap_or_default()
420        };
421
422        self.base.child(
423            h_flex()
424                .gap(DynamicSpacing::Base04.rems(cx))
425                .when(self.icon_position == Some(IconPosition::Start), |this| {
426                    this.children(self.icon.map(|icon| {
427                        ButtonIcon::new(icon)
428                            .disabled(is_disabled)
429                            .toggle_state(is_selected)
430                            .selected_icon(self.selected_icon)
431                            .selected_icon_color(self.selected_icon_color)
432                            .size(self.icon_size)
433                            .color(self.icon_color)
434                    }))
435                })
436                .child(
437                    h_flex()
438                        .when(
439                            self.key_binding_position == KeybindingPosition::Start,
440                            |this| this.flex_row_reverse(),
441                        )
442                        .gap(DynamicSpacing::Base06.rems(cx))
443                        .justify_between()
444                        .child(
445                            Label::new(label)
446                                .color(label_color)
447                                .size(self.label_size.unwrap_or_default())
448                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
449                                .when(self.truncate, |this| this.truncate()),
450                        )
451                        .children(self.key_binding),
452                )
453                .when(self.icon_position != Some(IconPosition::Start), |this| {
454                    this.children(self.icon.map(|icon| {
455                        ButtonIcon::new(icon)
456                            .disabled(is_disabled)
457                            .toggle_state(is_selected)
458                            .selected_icon(self.selected_icon)
459                            .selected_icon_color(self.selected_icon_color)
460                            .size(self.icon_size)
461                            .color(self.icon_color)
462                    }))
463                }),
464        )
465    }
466}
467
468// View this component preview using `workspace: open component-preview`
469impl ComponentPreview for Button {
470    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
471        v_flex()
472            .gap_6()
473            .children(vec![
474                example_group_with_title(
475                    "Button Styles",
476                    vec![
477                        single_example(
478                            "Default",
479                            Button::new("default", "Default").into_any_element(),
480                        ),
481                        single_example(
482                            "Filled",
483                            Button::new("filled", "Filled")
484                                .style(ButtonStyle::Filled)
485                                .into_any_element(),
486                        ),
487                        single_example(
488                            "Subtle",
489                            Button::new("outline", "Subtle")
490                                .style(ButtonStyle::Subtle)
491                                .into_any_element(),
492                        ),
493                        single_example(
494                            "Tinted",
495                            Button::new("tinted_accent_style", "Accent")
496                                .style(ButtonStyle::Tinted(TintColor::Accent))
497                                .into_any_element(),
498                        ),
499                        single_example(
500                            "Transparent",
501                            Button::new("transparent", "Transparent")
502                                .style(ButtonStyle::Transparent)
503                                .into_any_element(),
504                        ),
505                    ],
506                ),
507                example_group_with_title(
508                    "Tint Styles",
509                    vec![
510                        single_example(
511                            "Accent",
512                            Button::new("tinted_accent", "Accent")
513                                .style(ButtonStyle::Tinted(TintColor::Accent))
514                                .into_any_element(),
515                        ),
516                        single_example(
517                            "Error",
518                            Button::new("tinted_negative", "Error")
519                                .style(ButtonStyle::Tinted(TintColor::Error))
520                                .into_any_element(),
521                        ),
522                        single_example(
523                            "Warning",
524                            Button::new("tinted_warning", "Warning")
525                                .style(ButtonStyle::Tinted(TintColor::Warning))
526                                .into_any_element(),
527                        ),
528                        single_example(
529                            "Success",
530                            Button::new("tinted_positive", "Success")
531                                .style(ButtonStyle::Tinted(TintColor::Success))
532                                .into_any_element(),
533                        ),
534                    ],
535                ),
536                example_group_with_title(
537                    "Special States",
538                    vec![
539                        single_example(
540                            "Default",
541                            Button::new("default_state", "Default").into_any_element(),
542                        ),
543                        single_example(
544                            "Disabled",
545                            Button::new("disabled", "Disabled")
546                                .disabled(true)
547                                .into_any_element(),
548                        ),
549                        single_example(
550                            "Selected",
551                            Button::new("selected", "Selected")
552                                .toggle_state(true)
553                                .into_any_element(),
554                        ),
555                    ],
556                ),
557                example_group_with_title(
558                    "Buttons with Icons",
559                    vec![
560                        single_example(
561                            "Icon Start",
562                            Button::new("icon_start", "Icon Start")
563                                .icon(IconName::Check)
564                                .icon_position(IconPosition::Start)
565                                .into_any_element(),
566                        ),
567                        single_example(
568                            "Icon End",
569                            Button::new("icon_end", "Icon End")
570                                .icon(IconName::Check)
571                                .icon_position(IconPosition::End)
572                                .into_any_element(),
573                        ),
574                        single_example(
575                            "Icon Color",
576                            Button::new("icon_color", "Icon Color")
577                                .icon(IconName::Check)
578                                .icon_color(Color::Accent)
579                                .into_any_element(),
580                        ),
581                    ],
582                ),
583            ])
584            .into_any_element()
585    }
586}