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