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}
101
102impl Button {
103 /// Creates a new [`Button`] with a specified identifier and label.
104 ///
105 /// This is the primary constructor for a [`Button`] component. It initializes
106 /// the button with the provided identifier and label text, setting all other
107 /// properties to their default values, which can be customized using the
108 /// builder pattern methods provided by this struct.
109 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
110 Self {
111 base: ButtonLike::new(id),
112 label: label.into(),
113 label_color: None,
114 label_size: None,
115 selected_label: None,
116 selected_label_color: None,
117 icon: None,
118 icon_position: None,
119 icon_size: None,
120 icon_color: None,
121 selected_icon: None,
122 selected_icon_color: None,
123 key_binding: None,
124 key_binding_position: KeybindingPosition::default(),
125 alpha: None,
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
211impl Toggleable for Button {
212 /// Sets the selected state of the button.
213 ///
214 /// This method allows the selection state of the button to be specified.
215 /// It modifies the button's appearance to reflect its selected state.
216 ///
217 /// # Examples
218 ///
219 /// ```
220 /// use ui::prelude::*;
221 ///
222 /// Button::new("button_id", "Click me!")
223 /// .selected(true)
224 /// .on_click(|event, cx| {
225 /// // Handle click event
226 /// });
227 /// ```
228 ///
229 /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
230 fn toggle_state(mut self, selected: bool) -> Self {
231 self.base = self.base.toggle_state(selected);
232 self
233 }
234}
235
236impl SelectableButton for Button {
237 /// Sets the style for the button when selected.
238 ///
239 /// # Examples
240 ///
241 /// ```
242 /// use ui::prelude::*;
243 /// use ui::TintColor;
244 ///
245 /// Button::new("button_id", "Click me!")
246 /// .selected(true)
247 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
248 /// .on_click(|event, cx| {
249 /// // Handle click event
250 /// });
251 /// ```
252 /// This results in a button with a blue tinted background when selected.
253 fn selected_style(mut self, style: ButtonStyle) -> Self {
254 self.base = self.base.selected_style(style);
255 self
256 }
257}
258
259impl Disableable for Button {
260 /// Disables the button.
261 ///
262 /// This method allows the button to be disabled. When a button is disabled,
263 /// it doesn't react to user interactions and its appearance is updated to reflect this.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// use ui::prelude::*;
269 ///
270 /// Button::new("button_id", "Click me!")
271 /// .disabled(true)
272 /// .on_click(|event, cx| {
273 /// // Handle click event
274 /// });
275 /// ```
276 ///
277 /// This results in a button that is disabled and does not respond to click events.
278 fn disabled(mut self, disabled: bool) -> Self {
279 self.base = self.base.disabled(disabled);
280 self
281 }
282}
283
284impl Clickable for Button {
285 /// Sets the click event handler for the button.
286 fn on_click(
287 mut self,
288 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
289 ) -> Self {
290 self.base = self.base.on_click(handler);
291 self
292 }
293
294 fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
295 self.base = self.base.cursor_style(cursor_style);
296 self
297 }
298}
299
300impl FixedWidth for Button {
301 /// Sets a fixed width for the button.
302 ///
303 /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
304 /// Sets a fixed width for the button.
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// use ui::prelude::*;
310 ///
311 /// Button::new("button_id", "Click me!")
312 /// .width(px(100.).into())
313 /// .on_click(|event, cx| {
314 /// // Handle click event
315 /// });
316 /// ```
317 ///
318 /// This sets the button's width to be exactly 100 pixels.
319 fn width(mut self, width: DefiniteLength) -> Self {
320 self.base = self.base.width(width);
321 self
322 }
323
324 /// Sets the button to occupy the full width of its container.
325 ///
326 /// # Examples
327 ///
328 /// ```
329 /// use ui::prelude::*;
330 ///
331 /// Button::new("button_id", "Click me!")
332 /// .full_width()
333 /// .on_click(|event, cx| {
334 /// // Handle click event
335 /// });
336 /// ```
337 ///
338 /// This stretches the button to the full width of its container.
339 fn full_width(mut self) -> Self {
340 self.base = self.base.full_width();
341 self
342 }
343}
344
345impl ButtonCommon for Button {
346 /// Sets the button's id.
347 fn id(&self) -> &ElementId {
348 self.base.id()
349 }
350
351 /// Sets the visual style of the button using a [`ButtonStyle`].
352 fn style(mut self, style: ButtonStyle) -> Self {
353 self.base = self.base.style(style);
354 self
355 }
356
357 /// Sets the button's size using a [`ButtonSize`].
358 fn size(mut self, size: ButtonSize) -> Self {
359 self.base = self.base.size(size);
360 self
361 }
362
363 /// Sets a tooltip for the button.
364 ///
365 /// This method allows a tooltip to be set for the button. The tooltip is a function that
366 /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The
367 /// tooltip is displayed when the user hovers over the button.
368 ///
369 /// # Examples
370 ///
371 /// ```
372 /// use ui::prelude::*;
373 /// use ui::Tooltip;
374 ///
375 /// Button::new("button_id", "Click me!")
376 /// .tooltip(Tooltip::text_f("This is a tooltip", cx))
377 /// .on_click(|event, cx| {
378 /// // Handle click event
379 /// });
380 /// ```
381 ///
382 /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
383 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
384 self.base = self.base.tooltip(tooltip);
385 self
386 }
387
388 fn layer(mut self, elevation: ElevationIndex) -> Self {
389 self.base = self.base.layer(elevation);
390 self
391 }
392}
393
394impl RenderOnce for Button {
395 #[allow(refining_impl_trait)]
396 fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
397 let is_disabled = self.base.disabled;
398 let is_selected = self.base.selected;
399
400 let label = self
401 .selected_label
402 .filter(|_| is_selected)
403 .unwrap_or(self.label);
404
405 let label_color = if is_disabled {
406 Color::Disabled
407 } else if is_selected {
408 self.selected_label_color.unwrap_or(Color::Selected)
409 } else {
410 self.label_color.unwrap_or_default()
411 };
412
413 self.base.child(
414 h_flex()
415 .gap(DynamicSpacing::Base04.rems(cx))
416 .when(self.icon_position == Some(IconPosition::Start), |this| {
417 this.children(self.icon.map(|icon| {
418 ButtonIcon::new(icon)
419 .disabled(is_disabled)
420 .toggle_state(is_selected)
421 .selected_icon(self.selected_icon)
422 .selected_icon_color(self.selected_icon_color)
423 .size(self.icon_size)
424 .color(self.icon_color)
425 }))
426 })
427 .child(
428 h_flex()
429 .when(
430 self.key_binding_position == KeybindingPosition::Start,
431 |this| this.flex_row_reverse(),
432 )
433 .gap(DynamicSpacing::Base06.rems(cx))
434 .justify_between()
435 .child(
436 Label::new(label)
437 .color(label_color)
438 .size(self.label_size.unwrap_or_default())
439 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
440 .line_height_style(LineHeightStyle::UiLabel),
441 )
442 .children(self.key_binding),
443 )
444 .when(self.icon_position != Some(IconPosition::Start), |this| {
445 this.children(self.icon.map(|icon| {
446 ButtonIcon::new(icon)
447 .disabled(is_disabled)
448 .toggle_state(is_selected)
449 .selected_icon(self.selected_icon)
450 .selected_icon_color(self.selected_icon_color)
451 .size(self.icon_size)
452 .color(self.icon_color)
453 }))
454 }),
455 )
456 }
457}
458
459// View this component preview using `workspace: open component-preview`
460impl ComponentPreview for Button {
461 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
462 v_flex()
463 .gap_6()
464 .children(vec![
465 example_group_with_title(
466 "Button Styles",
467 vec![
468 single_example(
469 "Default",
470 Button::new("default", "Default").into_any_element(),
471 ),
472 single_example(
473 "Filled",
474 Button::new("filled", "Filled")
475 .style(ButtonStyle::Filled)
476 .into_any_element(),
477 ),
478 single_example(
479 "Subtle",
480 Button::new("outline", "Subtle")
481 .style(ButtonStyle::Subtle)
482 .into_any_element(),
483 ),
484 single_example(
485 "Tinted",
486 Button::new("tinted_accent_style", "Accent")
487 .style(ButtonStyle::Tinted(TintColor::Accent))
488 .into_any_element(),
489 ),
490 single_example(
491 "Transparent",
492 Button::new("transparent", "Transparent")
493 .style(ButtonStyle::Transparent)
494 .into_any_element(),
495 ),
496 ],
497 ),
498 example_group_with_title(
499 "Tint Styles",
500 vec![
501 single_example(
502 "Accent",
503 Button::new("tinted_accent", "Accent")
504 .style(ButtonStyle::Tinted(TintColor::Accent))
505 .into_any_element(),
506 ),
507 single_example(
508 "Error",
509 Button::new("tinted_negative", "Error")
510 .style(ButtonStyle::Tinted(TintColor::Error))
511 .into_any_element(),
512 ),
513 single_example(
514 "Warning",
515 Button::new("tinted_warning", "Warning")
516 .style(ButtonStyle::Tinted(TintColor::Warning))
517 .into_any_element(),
518 ),
519 single_example(
520 "Success",
521 Button::new("tinted_positive", "Success")
522 .style(ButtonStyle::Tinted(TintColor::Success))
523 .into_any_element(),
524 ),
525 ],
526 ),
527 example_group_with_title(
528 "Special States",
529 vec![
530 single_example(
531 "Default",
532 Button::new("default_state", "Default").into_any_element(),
533 ),
534 single_example(
535 "Disabled",
536 Button::new("disabled", "Disabled")
537 .disabled(true)
538 .into_any_element(),
539 ),
540 single_example(
541 "Selected",
542 Button::new("selected", "Selected")
543 .toggle_state(true)
544 .into_any_element(),
545 ),
546 ],
547 ),
548 example_group_with_title(
549 "Buttons with Icons",
550 vec![
551 single_example(
552 "Icon Start",
553 Button::new("icon_start", "Icon Start")
554 .icon(IconName::Check)
555 .icon_position(IconPosition::Start)
556 .into_any_element(),
557 ),
558 single_example(
559 "Icon End",
560 Button::new("icon_end", "Icon End")
561 .icon(IconName::Check)
562 .icon_position(IconPosition::End)
563 .into_any_element(),
564 ),
565 single_example(
566 "Icon Color",
567 Button::new("icon_color", "Icon Color")
568 .icon(IconName::Check)
569 .icon_color(Color::Accent)
570 .into_any_element(),
571 ),
572 ],
573 ),
574 ])
575 .into_any_element()
576 }
577}