button.rs

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