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