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