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