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