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 const 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 const 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 const 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
477impl Component for Button {
478 fn scope() -> ComponentScope {
479 ComponentScope::Input
480 }
481
482 fn sort_name() -> &'static str {
483 "ButtonA"
484 }
485
486 fn description() -> Option<&'static str> {
487 Some("A button triggers an event or action.")
488 }
489
490 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
491 Some(
492 v_flex()
493 .gap_6()
494 .children(vec![
495 example_group_with_title(
496 "Button Styles",
497 vec![
498 single_example(
499 "Default",
500 Button::new("default", "Default").into_any_element(),
501 ),
502 single_example(
503 "Filled",
504 Button::new("filled", "Filled")
505 .style(ButtonStyle::Filled)
506 .into_any_element(),
507 ),
508 single_example(
509 "Subtle",
510 Button::new("outline", "Subtle")
511 .style(ButtonStyle::Subtle)
512 .into_any_element(),
513 ),
514 single_example(
515 "Tinted",
516 Button::new("tinted_accent_style", "Accent")
517 .style(ButtonStyle::Tinted(TintColor::Accent))
518 .into_any_element(),
519 ),
520 single_example(
521 "Transparent",
522 Button::new("transparent", "Transparent")
523 .style(ButtonStyle::Transparent)
524 .into_any_element(),
525 ),
526 ],
527 ),
528 example_group_with_title(
529 "Tint Styles",
530 vec![
531 single_example(
532 "Accent",
533 Button::new("tinted_accent", "Accent")
534 .style(ButtonStyle::Tinted(TintColor::Accent))
535 .into_any_element(),
536 ),
537 single_example(
538 "Error",
539 Button::new("tinted_negative", "Error")
540 .style(ButtonStyle::Tinted(TintColor::Error))
541 .into_any_element(),
542 ),
543 single_example(
544 "Warning",
545 Button::new("tinted_warning", "Warning")
546 .style(ButtonStyle::Tinted(TintColor::Warning))
547 .into_any_element(),
548 ),
549 single_example(
550 "Success",
551 Button::new("tinted_positive", "Success")
552 .style(ButtonStyle::Tinted(TintColor::Success))
553 .into_any_element(),
554 ),
555 ],
556 ),
557 example_group_with_title(
558 "Special States",
559 vec![
560 single_example(
561 "Default",
562 Button::new("default_state", "Default").into_any_element(),
563 ),
564 single_example(
565 "Disabled",
566 Button::new("disabled", "Disabled")
567 .disabled(true)
568 .into_any_element(),
569 ),
570 single_example(
571 "Selected",
572 Button::new("selected", "Selected")
573 .toggle_state(true)
574 .into_any_element(),
575 ),
576 ],
577 ),
578 example_group_with_title(
579 "Buttons with Icons",
580 vec![
581 single_example(
582 "Icon Start",
583 Button::new("icon_start", "Icon Start")
584 .icon(IconName::Check)
585 .icon_position(IconPosition::Start)
586 .into_any_element(),
587 ),
588 single_example(
589 "Icon End",
590 Button::new("icon_end", "Icon End")
591 .icon(IconName::Check)
592 .icon_position(IconPosition::End)
593 .into_any_element(),
594 ),
595 single_example(
596 "Icon Color",
597 Button::new("icon_color", "Icon Color")
598 .icon(IconName::Check)
599 .icon_color(Color::Accent)
600 .into_any_element(),
601 ),
602 ],
603 ),
604 ])
605 .into_any_element(),
606 )
607 }
608}