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