button.rs

  1use component::{example_group_with_title, single_example, ComponentPreview};
  2use gpui::{AnyElement, AnyView, DefiniteLength};
  3use ui_macros::IntoComponent;
  4
  5use crate::{
  6    prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
  7    KeybindingPosition, TintColor,
  8};
  9use crate::{
 10    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
 11};
 12
 13use super::button_icon::ButtonIcon;
 14
 15/// An element that creates a button with a label and an optional icon.
 16///
 17/// Common buttons:
 18/// - Label, Icon + Label: [`Button`] (this component)
 19/// - Icon only: [`IconButton`]
 20/// - Custom: [`ButtonLike`]
 21///
 22/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
 23/// [`ButtonLike`] directly.
 24///
 25/// # Examples
 26///
 27/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
 28/// indicates what action will be performed when the button is clicked.
 29///
 30/// ```
 31/// use ui::prelude::*;
 32///
 33/// Button::new("button_id", "Click me!")
 34///     .on_click(|event, cx| {
 35///         // Handle click event
 36///     });
 37/// ```
 38///
 39/// **A toggleable button**, is typically used in scenarios such as a toolbar,
 40/// where the button's state indicates whether a feature is enabled or not, or
 41/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
 42///
 43/// ```
 44/// use ui::prelude::*;
 45///
 46/// Button::new("button_id", "Click me!")
 47///     .icon(IconName::Check)
 48///     .selected(true)
 49///     .on_click(|event, cx| {
 50///         // Handle click event
 51///     });
 52/// ```
 53///
 54/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
 55///
 56/// ```
 57/// use ui::prelude::*;
 58/// use ui::TintColor;
 59///
 60/// Button::new("button_id", "Click me!")
 61///     .selected(true)
 62///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 63///     .on_click(|event, cx| {
 64///         // Handle click event
 65///     });
 66/// ```
 67/// This will create a button with a blue tinted background when selected.
 68///
 69/// **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.
 70/// The button's content, including text and icons, is centered by default.
 71///
 72/// ```
 73/// use ui::prelude::*;
 74///
 75/// let button = Button::new("button_id", "Click me!")
 76///     .full_width()
 77///     .on_click(|event, cx| {
 78///         // Handle click event
 79///     });
 80/// ```
 81///
 82#[derive(IntoElement, IntoComponent)]
 83#[component(scope = "input")]
 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
459// View this component preview using `workspace: open component-preview`
460impl ComponentPreview for Button {
461    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
462        v_flex()
463            .gap_6()
464            .children(vec![
465                example_group_with_title(
466                    "Button Styles",
467                    vec![
468                        single_example(
469                            "Default",
470                            Button::new("default", "Default").into_any_element(),
471                        ),
472                        single_example(
473                            "Filled",
474                            Button::new("filled", "Filled")
475                                .style(ButtonStyle::Filled)
476                                .into_any_element(),
477                        ),
478                        single_example(
479                            "Subtle",
480                            Button::new("outline", "Subtle")
481                                .style(ButtonStyle::Subtle)
482                                .into_any_element(),
483                        ),
484                        single_example(
485                            "Tinted",
486                            Button::new("tinted_accent_style", "Accent")
487                                .style(ButtonStyle::Tinted(TintColor::Accent))
488                                .into_any_element(),
489                        ),
490                        single_example(
491                            "Transparent",
492                            Button::new("transparent", "Transparent")
493                                .style(ButtonStyle::Transparent)
494                                .into_any_element(),
495                        ),
496                    ],
497                ),
498                example_group_with_title(
499                    "Tint Styles",
500                    vec![
501                        single_example(
502                            "Accent",
503                            Button::new("tinted_accent", "Accent")
504                                .style(ButtonStyle::Tinted(TintColor::Accent))
505                                .into_any_element(),
506                        ),
507                        single_example(
508                            "Error",
509                            Button::new("tinted_negative", "Error")
510                                .style(ButtonStyle::Tinted(TintColor::Error))
511                                .into_any_element(),
512                        ),
513                        single_example(
514                            "Warning",
515                            Button::new("tinted_warning", "Warning")
516                                .style(ButtonStyle::Tinted(TintColor::Warning))
517                                .into_any_element(),
518                        ),
519                        single_example(
520                            "Success",
521                            Button::new("tinted_positive", "Success")
522                                .style(ButtonStyle::Tinted(TintColor::Success))
523                                .into_any_element(),
524                        ),
525                    ],
526                ),
527                example_group_with_title(
528                    "Special States",
529                    vec![
530                        single_example(
531                            "Default",
532                            Button::new("default_state", "Default").into_any_element(),
533                        ),
534                        single_example(
535                            "Disabled",
536                            Button::new("disabled", "Disabled")
537                                .disabled(true)
538                                .into_any_element(),
539                        ),
540                        single_example(
541                            "Selected",
542                            Button::new("selected", "Selected")
543                                .toggle_state(true)
544                                .into_any_element(),
545                        ),
546                    ],
547                ),
548                example_group_with_title(
549                    "Buttons with Icons",
550                    vec![
551                        single_example(
552                            "Icon Start",
553                            Button::new("icon_start", "Icon Start")
554                                .icon(IconName::Check)
555                                .icon_position(IconPosition::Start)
556                                .into_any_element(),
557                        ),
558                        single_example(
559                            "Icon End",
560                            Button::new("icon_end", "Icon End")
561                                .icon(IconName::Check)
562                                .icon_position(IconPosition::End)
563                                .into_any_element(),
564                        ),
565                        single_example(
566                            "Icon Color",
567                            Button::new("icon_color", "Icon Color")
568                                .icon(IconName::Check)
569                                .icon_color(Color::Accent)
570                                .into_any_element(),
571                        ),
572                    ],
573                ),
574            ])
575            .into_any_element()
576    }
577}