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