1use crate::component_prelude::*;
2use gpui::{AnyElement, AnyView, DefiniteLength};
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 label = self
365 .selected_label
366 .filter(|_| is_selected)
367 .unwrap_or(self.label);
368
369 let label_color = if is_disabled {
370 Color::Disabled
371 } else if is_selected {
372 self.selected_label_color.unwrap_or(Color::Selected)
373 } else {
374 self.label_color.unwrap_or_default()
375 };
376
377 self.base.child(
378 h_flex()
379 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
380 .gap(DynamicSpacing::Base04.rems(cx))
381 .when_some(self.start_icon, |this, icon| {
382 this.child(if is_disabled {
383 icon.color(Color::Disabled)
384 } else {
385 icon
386 })
387 })
388 .child(
389 h_flex()
390 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
391 .when(
392 self.key_binding_position == KeybindingPosition::Start,
393 |this| this.flex_row_reverse(),
394 )
395 .gap(DynamicSpacing::Base06.rems(cx))
396 .justify_between()
397 .child(
398 Label::new(label)
399 .color(label_color)
400 .size(self.label_size.unwrap_or_default())
401 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
402 .when(self.truncate, |this| this.truncate()),
403 )
404 .children(self.key_binding),
405 )
406 .when_some(self.end_icon, |this, icon| {
407 this.child(if is_disabled {
408 icon.color(Color::Disabled)
409 } else {
410 icon
411 })
412 }),
413 )
414 }
415}
416
417impl Component for Button {
418 fn scope() -> ComponentScope {
419 ComponentScope::Input
420 }
421
422 fn sort_name() -> &'static str {
423 "ButtonA"
424 }
425
426 fn description() -> Option<&'static str> {
427 Some("A button triggers an event or action.")
428 }
429
430 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
431 Some(
432 v_flex()
433 .gap_6()
434 .children(vec![
435 example_group_with_title(
436 "Button Styles",
437 vec![
438 single_example(
439 "Default",
440 Button::new("default", "Default").into_any_element(),
441 ),
442 single_example(
443 "Filled",
444 Button::new("filled", "Filled")
445 .style(ButtonStyle::Filled)
446 .into_any_element(),
447 ),
448 single_example(
449 "Subtle",
450 Button::new("outline", "Subtle")
451 .style(ButtonStyle::Subtle)
452 .into_any_element(),
453 ),
454 single_example(
455 "Tinted",
456 Button::new("tinted_accent_style", "Accent")
457 .style(ButtonStyle::Tinted(TintColor::Accent))
458 .into_any_element(),
459 ),
460 single_example(
461 "Transparent",
462 Button::new("transparent", "Transparent")
463 .style(ButtonStyle::Transparent)
464 .into_any_element(),
465 ),
466 ],
467 ),
468 example_group_with_title(
469 "Tint Styles",
470 vec![
471 single_example(
472 "Accent",
473 Button::new("tinted_accent", "Accent")
474 .style(ButtonStyle::Tinted(TintColor::Accent))
475 .into_any_element(),
476 ),
477 single_example(
478 "Error",
479 Button::new("tinted_negative", "Error")
480 .style(ButtonStyle::Tinted(TintColor::Error))
481 .into_any_element(),
482 ),
483 single_example(
484 "Warning",
485 Button::new("tinted_warning", "Warning")
486 .style(ButtonStyle::Tinted(TintColor::Warning))
487 .into_any_element(),
488 ),
489 single_example(
490 "Success",
491 Button::new("tinted_positive", "Success")
492 .style(ButtonStyle::Tinted(TintColor::Success))
493 .into_any_element(),
494 ),
495 ],
496 ),
497 example_group_with_title(
498 "Special States",
499 vec![
500 single_example(
501 "Default",
502 Button::new("default_state", "Default").into_any_element(),
503 ),
504 single_example(
505 "Disabled",
506 Button::new("disabled", "Disabled")
507 .disabled(true)
508 .into_any_element(),
509 ),
510 single_example(
511 "Selected",
512 Button::new("selected", "Selected")
513 .toggle_state(true)
514 .into_any_element(),
515 ),
516 ],
517 ),
518 example_group_with_title(
519 "Buttons with Icons",
520 vec![
521 single_example(
522 "Start Icon",
523 Button::new("icon_start", "Start Icon")
524 .start_icon(Icon::new(IconName::Check))
525 .into_any_element(),
526 ),
527 single_example(
528 "End Icon",
529 Button::new("icon_end", "End Icon")
530 .end_icon(Icon::new(IconName::Check))
531 .into_any_element(),
532 ),
533 single_example(
534 "Both Icons",
535 Button::new("both_icons", "Both Icons")
536 .start_icon(Icon::new(IconName::Check))
537 .end_icon(Icon::new(IconName::ChevronDown))
538 .into_any_element(),
539 ),
540 single_example(
541 "Icon Color",
542 Button::new("icon_color", "Icon Color")
543 .start_icon(Icon::new(IconName::Check).color(Color::Accent))
544 .into_any_element(),
545 ),
546 ],
547 ),
548 ])
549 .into_any_element(),
550 )
551 }
552}