button.rs

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