button.rs

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