button.rs

  1use crate::component_prelude::*;
  2use gpui::{AnyElement, AnyView, DefiniteLength, Role};
  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 id = self.id().clone();
365        let label = self
366            .selected_label
367            .filter(|_| is_selected)
368            .unwrap_or(self.label);
369
370        let label_color = if is_disabled {
371            Color::Disabled
372        } else if is_selected {
373            self.selected_label_color.unwrap_or(Color::Selected)
374        } else {
375            self.label_color.unwrap_or_default()
376        };
377
378        self.base.child(
379            h_flex()
380                .id(id)
381                .role(Role::Button)
382                .aria_label(&label)
383                .when(self.truncate, |this| this.min_w_0().overflow_hidden())
384                .gap(DynamicSpacing::Base04.rems(cx))
385                .when_some(self.start_icon, |this, icon| {
386                    this.child(if is_disabled {
387                        icon.color(Color::Disabled)
388                    } else {
389                        icon
390                    })
391                })
392                .child(
393                    h_flex()
394                        .when(self.truncate, |this| this.min_w_0().overflow_hidden())
395                        .when(
396                            self.key_binding_position == KeybindingPosition::Start,
397                            |this| this.flex_row_reverse(),
398                        )
399                        .gap(DynamicSpacing::Base06.rems(cx))
400                        .justify_between()
401                        .child(
402                            Label::new(label)
403                                .color(label_color)
404                                .size(self.label_size.unwrap_or_default())
405                                .when_some(self.alpha, |this, alpha| this.alpha(alpha))
406                                .when(self.truncate, |this| this.truncate()),
407                        )
408                        .children(self.key_binding),
409                )
410                .when_some(self.end_icon, |this, icon| {
411                    this.child(if is_disabled {
412                        icon.color(Color::Disabled)
413                    } else {
414                        icon
415                    })
416                }),
417        )
418    }
419}
420
421impl Component for Button {
422    fn scope() -> ComponentScope {
423        ComponentScope::Input
424    }
425
426    fn sort_name() -> &'static str {
427        "ButtonA"
428    }
429
430    fn description() -> Option<&'static str> {
431        Some("A button triggers an event or action.")
432    }
433
434    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
435        Some(
436            v_flex()
437                .gap_6()
438                .children(vec![
439                    example_group_with_title(
440                        "Button Styles",
441                        vec![
442                            single_example(
443                                "Default",
444                                Button::new("default", "Default").into_any_element(),
445                            ),
446                            single_example(
447                                "Filled",
448                                Button::new("filled", "Filled")
449                                    .style(ButtonStyle::Filled)
450                                    .into_any_element(),
451                            ),
452                            single_example(
453                                "Subtle",
454                                Button::new("outline", "Subtle")
455                                    .style(ButtonStyle::Subtle)
456                                    .into_any_element(),
457                            ),
458                            single_example(
459                                "Tinted",
460                                Button::new("tinted_accent_style", "Accent")
461                                    .style(ButtonStyle::Tinted(TintColor::Accent))
462                                    .into_any_element(),
463                            ),
464                            single_example(
465                                "Transparent",
466                                Button::new("transparent", "Transparent")
467                                    .style(ButtonStyle::Transparent)
468                                    .into_any_element(),
469                            ),
470                        ],
471                    ),
472                    example_group_with_title(
473                        "Tint Styles",
474                        vec![
475                            single_example(
476                                "Accent",
477                                Button::new("tinted_accent", "Accent")
478                                    .style(ButtonStyle::Tinted(TintColor::Accent))
479                                    .into_any_element(),
480                            ),
481                            single_example(
482                                "Error",
483                                Button::new("tinted_negative", "Error")
484                                    .style(ButtonStyle::Tinted(TintColor::Error))
485                                    .into_any_element(),
486                            ),
487                            single_example(
488                                "Warning",
489                                Button::new("tinted_warning", "Warning")
490                                    .style(ButtonStyle::Tinted(TintColor::Warning))
491                                    .into_any_element(),
492                            ),
493                            single_example(
494                                "Success",
495                                Button::new("tinted_positive", "Success")
496                                    .style(ButtonStyle::Tinted(TintColor::Success))
497                                    .into_any_element(),
498                            ),
499                        ],
500                    ),
501                    example_group_with_title(
502                        "Special States",
503                        vec![
504                            single_example(
505                                "Default",
506                                Button::new("default_state", "Default").into_any_element(),
507                            ),
508                            single_example(
509                                "Disabled",
510                                Button::new("disabled", "Disabled")
511                                    .disabled(true)
512                                    .into_any_element(),
513                            ),
514                            single_example(
515                                "Selected",
516                                Button::new("selected", "Selected")
517                                    .toggle_state(true)
518                                    .into_any_element(),
519                            ),
520                        ],
521                    ),
522                    example_group_with_title(
523                        "Buttons with Icons",
524                        vec![
525                            single_example(
526                                "Start Icon",
527                                Button::new("icon_start", "Start Icon")
528                                    .start_icon(Icon::new(IconName::Check))
529                                    .into_any_element(),
530                            ),
531                            single_example(
532                                "End Icon",
533                                Button::new("icon_end", "End Icon")
534                                    .end_icon(Icon::new(IconName::Check))
535                                    .into_any_element(),
536                            ),
537                            single_example(
538                                "Both Icons",
539                                Button::new("both_icons", "Both Icons")
540                                    .start_icon(Icon::new(IconName::Check))
541                                    .end_icon(Icon::new(IconName::ChevronDown))
542                                    .into_any_element(),
543                            ),
544                            single_example(
545                                "Icon Color",
546                                Button::new("icon_color", "Icon Color")
547                                    .start_icon(Icon::new(IconName::Check).color(Color::Accent))
548                                    .into_any_element(),
549                            ),
550                        ],
551                    ),
552                ])
553                .into_any_element(),
554        )
555    }
556}