button.rs

  1#![allow(missing_docs)]
  2use gpui::{AnyView, DefiniteLength};
  3
  4use crate::{
  5    prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, TintColor,
  6};
  7use crate::{
  8    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
  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, 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, 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, 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, cx| {
 76///         // Handle click event
 77///     });
 78/// ```
 79///
 80#[derive(IntoElement)]
 81pub struct Button {
 82    base: ButtonLike,
 83    label: SharedString,
 84    label_color: Option<Color>,
 85    label_size: Option<LabelSize>,
 86    selected_label: Option<SharedString>,
 87    selected_label_color: Option<Color>,
 88    icon: Option<IconName>,
 89    icon_position: Option<IconPosition>,
 90    icon_size: Option<IconSize>,
 91    icon_color: Option<Color>,
 92    selected_icon: Option<IconName>,
 93    selected_icon_color: Option<Color>,
 94    key_binding: Option<KeyBinding>,
 95    alpha: Option<f32>,
 96}
 97
 98impl Button {
 99    /// Creates a new [`Button`] with a specified identifier and label.
100    ///
101    /// This is the primary constructor for a [`Button`] component. It initializes
102    /// the button with the provided identifier and label text, setting all other
103    /// properties to their default values, which can be customized using the
104    /// builder pattern methods provided by this struct.
105    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
106        Self {
107            base: ButtonLike::new(id),
108            label: label.into(),
109            label_color: None,
110            label_size: None,
111            selected_label: None,
112            selected_label_color: None,
113            icon: None,
114            icon_position: None,
115            icon_size: None,
116            icon_color: None,
117            selected_icon: None,
118            selected_icon_color: None,
119            key_binding: None,
120            alpha: None,
121        }
122    }
123
124    /// Sets the color of the button's label.
125    pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
126        self.label_color = label_color.into();
127        self
128    }
129
130    /// Defines the size of the button's label.
131    pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
132        self.label_size = label_size.into();
133        self
134    }
135
136    /// Sets the label used when the button is in a selected state.
137    pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
138        self.selected_label = label.into().map(Into::into);
139        self
140    }
141
142    /// Sets the label color used when the button is in a selected state.
143    pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
144        self.selected_label_color = color.into();
145        self
146    }
147
148    /// Assigns an icon to the button.
149    pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
150        self.icon = icon.into();
151        self
152    }
153
154    /// Sets the position of the icon relative to the label.
155    pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
156        self.icon_position = icon_position.into();
157        self
158    }
159
160    /// Specifies the size of the button's icon.
161    pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
162        self.icon_size = icon_size.into();
163        self
164    }
165
166    /// Sets the color of the button's icon.
167    pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
168        self.icon_color = icon_color.into();
169        self
170    }
171
172    /// Chooses an icon to display when the button is in a selected state.
173    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
174        self.selected_icon = icon.into();
175        self
176    }
177
178    /// Sets the icon color used when the button is in a selected state.
179    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
180        self.selected_icon_color = color.into();
181        self
182    }
183
184    /// Display the keybinding that triggers the button action.
185    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
186        self.key_binding = key_binding.into();
187        self
188    }
189
190    /// Sets the alpha property of the color of label.
191    pub fn alpha(mut self, alpha: f32) -> Self {
192        self.alpha = Some(alpha);
193        self
194    }
195}
196
197impl Toggleable for Button {
198    /// Sets the selected state of the button.
199    ///
200    /// This method allows the selection state of the button to be specified.
201    /// It modifies the button's appearance to reflect its selected state.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use ui::prelude::*;
207    ///
208    /// Button::new("button_id", "Click me!")
209    ///     .selected(true)
210    ///     .on_click(|event, cx| {
211    ///         // Handle click event
212    ///     });
213    /// ```
214    ///
215    /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
216    fn toggle_state(mut self, selected: bool) -> Self {
217        self.base = self.base.toggle_state(selected);
218        self
219    }
220}
221
222impl SelectableButton for Button {
223    /// Sets the style for the button when selected.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use ui::prelude::*;
229    /// use ui::TintColor;
230    ///
231    /// Button::new("button_id", "Click me!")
232    ///     .selected(true)
233    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
234    ///     .on_click(|event, cx| {
235    ///         // Handle click event
236    ///     });
237    /// ```
238    /// This results in a button with a blue tinted background when selected.
239    fn selected_style(mut self, style: ButtonStyle) -> Self {
240        self.base = self.base.selected_style(style);
241        self
242    }
243}
244
245impl Disableable for Button {
246    /// Disables the button.
247    ///
248    /// This method allows the button to be disabled. When a button is disabled,
249    /// it doesn't react to user interactions and its appearance is updated to reflect this.
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use ui::prelude::*;
255    ///
256    /// Button::new("button_id", "Click me!")
257    ///     .disabled(true)
258    ///     .on_click(|event, cx| {
259    ///         // Handle click event
260    ///     });
261    /// ```
262    ///
263    /// This results in a button that is disabled and does not respond to click events.
264    fn disabled(mut self, disabled: bool) -> Self {
265        self.base = self.base.disabled(disabled);
266        self
267    }
268}
269
270impl Clickable for Button {
271    /// Sets the click event handler for the button.
272    fn on_click(
273        mut self,
274        handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
275    ) -> Self {
276        self.base = self.base.on_click(handler);
277        self
278    }
279
280    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
281        self.base = self.base.cursor_style(cursor_style);
282        self
283    }
284}
285
286impl FixedWidth for Button {
287    /// Sets a fixed width for the button.
288    ///
289    /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
290    /// Sets a fixed width for the button.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use ui::prelude::*;
296    ///
297    /// Button::new("button_id", "Click me!")
298    ///     .width(px(100.).into())
299    ///     .on_click(|event, cx| {
300    ///         // Handle click event
301    ///     });
302    /// ```
303    ///
304    /// This sets the button's width to be exactly 100 pixels.
305    fn width(mut self, width: DefiniteLength) -> Self {
306        self.base = self.base.width(width);
307        self
308    }
309
310    /// Sets the button to occupy the full width of its container.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use ui::prelude::*;
316    ///
317    /// Button::new("button_id", "Click me!")
318    ///     .full_width()
319    ///     .on_click(|event, cx| {
320    ///         // Handle click event
321    ///     });
322    /// ```
323    ///
324    /// This stretches the button to the full width of its container.
325    fn full_width(mut self) -> Self {
326        self.base = self.base.full_width();
327        self
328    }
329}
330
331impl ButtonCommon for Button {
332    /// Sets the button's id.
333    fn id(&self) -> &ElementId {
334        self.base.id()
335    }
336
337    /// Sets the visual style of the button using a [`ButtonStyle`].
338    fn style(mut self, style: ButtonStyle) -> Self {
339        self.base = self.base.style(style);
340        self
341    }
342
343    /// Sets the button's size using a [`ButtonSize`].
344    fn size(mut self, size: ButtonSize) -> Self {
345        self.base = self.base.size(size);
346        self
347    }
348
349    /// Sets a tooltip for the button.
350    ///
351    /// This method allows a tooltip to be set for the button. The tooltip is a function that
352    /// takes a mutable reference to a [`WindowContext`] and returns an [`AnyView`]. The tooltip
353    /// is displayed when the user hovers over the button.
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// use ui::prelude::*;
359    /// use ui::Tooltip;
360    ///
361    /// Button::new("button_id", "Click me!")
362    ///     .tooltip(move |cx| {
363    ///         Tooltip::text("This is a tooltip", cx)
364    ///     })
365    ///     .on_click(|event, cx| {
366    ///         // Handle click event
367    ///     });
368    /// ```
369    ///
370    /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
371    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
372        self.base = self.base.tooltip(tooltip);
373        self
374    }
375
376    fn layer(mut self, elevation: ElevationIndex) -> Self {
377        self.base = self.base.layer(elevation);
378        self
379    }
380}
381
382impl RenderOnce for Button {
383    #[allow(refining_impl_trait)]
384    fn render(self, cx: &mut WindowContext) -> ButtonLike {
385        let is_disabled = self.base.disabled;
386        let is_selected = self.base.selected;
387
388        let label = self
389            .selected_label
390            .filter(|_| is_selected)
391            .unwrap_or(self.label);
392
393        let label_color = if is_disabled {
394            Color::Disabled
395        } else if is_selected {
396            self.selected_label_color.unwrap_or(Color::Selected)
397        } else {
398            self.label_color.unwrap_or_default()
399        };
400
401        self.base.child(
402            h_flex()
403                .gap(DynamicSpacing::Base04.rems(cx))
404                .when(self.icon_position == Some(IconPosition::Start), |this| {
405                    this.children(self.icon.map(|icon| {
406                        ButtonIcon::new(icon)
407                            .disabled(is_disabled)
408                            .toggle_state(is_selected)
409                            .selected_icon(self.selected_icon)
410                            .selected_icon_color(self.selected_icon_color)
411                            .size(self.icon_size)
412                            .color(self.icon_color)
413                    }))
414                })
415                .child(
416                    h_flex()
417                        .gap(DynamicSpacing::Base06.rems(cx))
418                        .justify_between()
419                        .child(
420                            Label::new(label)
421                                .color(label_color)
422                                .size(self.label_size.unwrap_or_default())
423                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
424                                .line_height_style(LineHeightStyle::UiLabel),
425                        )
426                        .children(self.key_binding),
427                )
428                .when(self.icon_position != Some(IconPosition::Start), |this| {
429                    this.children(self.icon.map(|icon| {
430                        ButtonIcon::new(icon)
431                            .disabled(is_disabled)
432                            .toggle_state(is_selected)
433                            .selected_icon(self.selected_icon)
434                            .selected_icon_color(self.selected_icon_color)
435                            .size(self.icon_size)
436                            .color(self.icon_color)
437                    }))
438                }),
439        )
440    }
441}
442
443impl ComponentPreview for Button {
444    fn description() -> impl Into<Option<&'static str>> {
445        "A button allows users to take actions, and make choices, with a single tap."
446    }
447
448    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
449        vec![
450            example_group_with_title(
451                "Styles",
452                vec![
453                    single_example("Default", Button::new("default", "Default")),
454                    single_example(
455                        "Filled",
456                        Button::new("filled", "Filled").style(ButtonStyle::Filled),
457                    ),
458                    single_example(
459                        "Subtle",
460                        Button::new("outline", "Subtle").style(ButtonStyle::Subtle),
461                    ),
462                    single_example(
463                        "Transparent",
464                        Button::new("transparent", "Transparent").style(ButtonStyle::Transparent),
465                    ),
466                ],
467            ),
468            example_group_with_title(
469                "Tinted",
470                vec![
471                    single_example(
472                        "Accent",
473                        Button::new("tinted_accent", "Accent")
474                            .style(ButtonStyle::Tinted(TintColor::Accent)),
475                    ),
476                    single_example(
477                        "Error",
478                        Button::new("tinted_negative", "Error")
479                            .style(ButtonStyle::Tinted(TintColor::Error)),
480                    ),
481                    single_example(
482                        "Warning",
483                        Button::new("tinted_warning", "Warning")
484                            .style(ButtonStyle::Tinted(TintColor::Warning)),
485                    ),
486                    single_example(
487                        "Success",
488                        Button::new("tinted_positive", "Success")
489                            .style(ButtonStyle::Tinted(TintColor::Success)),
490                    ),
491                ],
492            ),
493            example_group_with_title(
494                "States",
495                vec![
496                    single_example("Default", Button::new("default_state", "Default")),
497                    single_example(
498                        "Disabled",
499                        Button::new("disabled", "Disabled").disabled(true),
500                    ),
501                    single_example(
502                        "Selected",
503                        Button::new("selected", "Selected").toggle_state(true),
504                    ),
505                ],
506            ),
507            example_group_with_title(
508                "With Icons",
509                vec![
510                    single_example(
511                        "Icon Start",
512                        Button::new("icon_start", "Icon Start")
513                            .icon(IconName::Check)
514                            .icon_position(IconPosition::Start),
515                    ),
516                    single_example(
517                        "Icon End",
518                        Button::new("icon_end", "Icon End")
519                            .icon(IconName::Check)
520                            .icon_position(IconPosition::End),
521                    ),
522                    single_example(
523                        "Icon Color",
524                        Button::new("icon_color", "Icon Color")
525                            .icon(IconName::Check)
526                            .icon_color(Color::Accent),
527                    ),
528                    single_example(
529                        "Tinted Icons",
530                        Button::new("tinted_icons", "Error")
531                            .style(ButtonStyle::Tinted(TintColor::Error))
532                            .color(Color::Error)
533                            .icon_color(Color::Error)
534                            .icon(IconName::Trash)
535                            .icon_position(IconPosition::Start),
536                    ),
537                ],
538            ),
539        ]
540    }
541}