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
289 }
290}
291
292impl Clickable for Button {
293 /// Sets the click event handler for the button.
294 fn on_click(
295 mut self,
296 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
297 ) -> Self {
298 self.base = self.base.on_click(handler);
299 self
300 }
301
302 fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
303 self.base = self.base.cursor_style(cursor_style);
304 self
305 }
306}
307
308impl FixedWidth for Button {
309 /// Sets a fixed width for the button.
310 ///
311 /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
312 /// Sets a fixed width for the button.
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// use ui::prelude::*;
318 ///
319 /// Button::new("button_id", "Click me!")
320 /// .width(px(100.))
321 /// .on_click(|event, window, cx| {
322 /// // Handle click event
323 /// });
324 /// ```
325 ///
326 /// This sets the button's width to be exactly 100 pixels.
327 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
328 self.base = self.base.width(width);
329 self
330 }
331
332 /// Sets the button to occupy the full width of its container.
333 ///
334 /// # Examples
335 ///
336 /// ```
337 /// use ui::prelude::*;
338 ///
339 /// Button::new("button_id", "Click me!")
340 /// .full_width()
341 /// .on_click(|event, window, cx| {
342 /// // Handle click event
343 /// });
344 /// ```
345 ///
346 /// This stretches the button to the full width of its container.
347 fn full_width(mut self) -> Self {
348 self.base = self.base.full_width();
349 self
350 }
351}
352
353impl ButtonCommon for Button {
354 /// Sets the button's id.
355 fn id(&self) -> &ElementId {
356 self.base.id()
357 }
358
359 /// Sets the visual style of the button using a [`ButtonStyle`].
360 fn style(mut self, style: ButtonStyle) -> Self {
361 self.base = self.base.style(style);
362 self
363 }
364
365 /// Sets the button's size using a [`ButtonSize`].
366 fn size(mut self, size: ButtonSize) -> Self {
367 self.base = self.base.size(size);
368 self
369 }
370
371 /// Sets a tooltip for the button.
372 ///
373 /// This method allows a tooltip to be set for the button. The tooltip is a function that
374 /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The
375 /// tooltip is displayed when the user hovers over the button.
376 ///
377 /// # Examples
378 ///
379 /// ```
380 /// use ui::prelude::*;
381 /// use ui::Tooltip;
382 ///
383 /// Button::new("button_id", "Click me!")
384 /// .tooltip(Tooltip::text("This is a tooltip"))
385 /// .on_click(|event, window, cx| {
386 /// // Handle click event
387 /// });
388 /// ```
389 ///
390 /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
391 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
392 self.base = self.base.tooltip(tooltip);
393 self
394 }
395
396 fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
397 self.base = self.base.tab_index(tab_index);
398 self
399 }
400
401 fn layer(mut self, elevation: ElevationIndex) -> Self {
402 self.base = self.base.layer(elevation);
403 self
404 }
405
406 fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
407 self.base = self.base.track_focus(focus_handle);
408 self
409 }
410}
411
412impl RenderOnce for Button {
413 #[allow(refining_impl_trait)]
414 fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
415 let is_disabled = self.base.disabled;
416 let is_selected = self.base.selected;
417
418 let label = self
419 .selected_label
420 .filter(|_| is_selected)
421 .unwrap_or(self.label);
422
423 let label_color = if is_disabled {
424 Color::Disabled
425 } else if is_selected {
426 self.selected_label_color.unwrap_or(Color::Selected)
427 } else {
428 self.label_color.unwrap_or_default()
429 };
430
431 self.base.child(
432 h_flex()
433 .gap(DynamicSpacing::Base04.rems(cx))
434 .when(self.icon_position == Some(IconPosition::Start), |this| {
435 this.children(self.icon.map(|icon| {
436 ButtonIcon::new(icon)
437 .disabled(is_disabled)
438 .toggle_state(is_selected)
439 .selected_icon(self.selected_icon)
440 .selected_icon_color(self.selected_icon_color)
441 .size(self.icon_size)
442 .color(self.icon_color)
443 }))
444 })
445 .child(
446 h_flex()
447 .when(
448 self.key_binding_position == KeybindingPosition::Start,
449 |this| this.flex_row_reverse(),
450 )
451 .gap(DynamicSpacing::Base06.rems(cx))
452 .justify_between()
453 .child(
454 Label::new(label)
455 .color(label_color)
456 .size(self.label_size.unwrap_or_default())
457 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
458 .when(self.truncate, |this| this.truncate()),
459 )
460 .children(self.key_binding),
461 )
462 .when(self.icon_position != Some(IconPosition::Start), |this| {
463 this.children(self.icon.map(|icon| {
464 ButtonIcon::new(icon)
465 .disabled(is_disabled)
466 .toggle_state(is_selected)
467 .selected_icon(self.selected_icon)
468 .selected_icon_color(self.selected_icon_color)
469 .size(self.icon_size)
470 .color(self.icon_color)
471 }))
472 }),
473 )
474 }
475}
476
477// View this component preview using `workspace: open component-preview`
478impl Component for Button {
479 fn scope() -> ComponentScope {
480 ComponentScope::Input
481 }
482
483 fn sort_name() -> &'static str {
484 "ButtonA"
485 }
486
487 fn description() -> Option<&'static str> {
488 Some("A button triggers an event or action.")
489 }
490
491 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
492 Some(
493 v_flex()
494 .gap_6()
495 .children(vec![
496 example_group_with_title(
497 "Button Styles",
498 vec![
499 single_example(
500 "Default",
501 Button::new("default", "Default").into_any_element(),
502 ),
503 single_example(
504 "Filled",
505 Button::new("filled", "Filled")
506 .style(ButtonStyle::Filled)
507 .into_any_element(),
508 ),
509 single_example(
510 "Subtle",
511 Button::new("outline", "Subtle")
512 .style(ButtonStyle::Subtle)
513 .into_any_element(),
514 ),
515 single_example(
516 "Tinted",
517 Button::new("tinted_accent_style", "Accent")
518 .style(ButtonStyle::Tinted(TintColor::Accent))
519 .into_any_element(),
520 ),
521 single_example(
522 "Transparent",
523 Button::new("transparent", "Transparent")
524 .style(ButtonStyle::Transparent)
525 .into_any_element(),
526 ),
527 ],
528 ),
529 example_group_with_title(
530 "Tint Styles",
531 vec![
532 single_example(
533 "Accent",
534 Button::new("tinted_accent", "Accent")
535 .style(ButtonStyle::Tinted(TintColor::Accent))
536 .into_any_element(),
537 ),
538 single_example(
539 "Error",
540 Button::new("tinted_negative", "Error")
541 .style(ButtonStyle::Tinted(TintColor::Error))
542 .into_any_element(),
543 ),
544 single_example(
545 "Warning",
546 Button::new("tinted_warning", "Warning")
547 .style(ButtonStyle::Tinted(TintColor::Warning))
548 .into_any_element(),
549 ),
550 single_example(
551 "Success",
552 Button::new("tinted_positive", "Success")
553 .style(ButtonStyle::Tinted(TintColor::Success))
554 .into_any_element(),
555 ),
556 ],
557 ),
558 example_group_with_title(
559 "Special States",
560 vec![
561 single_example(
562 "Default",
563 Button::new("default_state", "Default").into_any_element(),
564 ),
565 single_example(
566 "Disabled",
567 Button::new("disabled", "Disabled")
568 .disabled(true)
569 .into_any_element(),
570 ),
571 single_example(
572 "Selected",
573 Button::new("selected", "Selected")
574 .toggle_state(true)
575 .into_any_element(),
576 ),
577 ],
578 ),
579 example_group_with_title(
580 "Buttons with Icons",
581 vec![
582 single_example(
583 "Icon Start",
584 Button::new("icon_start", "Icon Start")
585 .icon(IconName::Check)
586 .icon_position(IconPosition::Start)
587 .into_any_element(),
588 ),
589 single_example(
590 "Icon End",
591 Button::new("icon_end", "Icon End")
592 .icon(IconName::Check)
593 .icon_position(IconPosition::End)
594 .into_any_element(),
595 ),
596 single_example(
597 "Icon Color",
598 Button::new("icon_color", "Icon Color")
599 .icon(IconName::Check)
600 .icon_color(Color::Accent)
601 .into_any_element(),
602 ),
603 ],
604 ),
605 ])
606 .into_any_element(),
607 )
608 }
609}