use crate::component_prelude::*;
use gpui::{AnyElement, AnyView, DefiniteLength};
use ui_macros::RegisterComponent;
use crate::traits::animation_ext::CommonAnimationExt;
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, Label};
use crate::{
Color, DynamicSpacing, ElevationIndex, KeyBinding, KeybindingPosition, TintColor, prelude::*,
};
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct Button {
base: ButtonLike,
label: SharedString,
label_color: Option<Color>,
label_size: Option<LabelSize>,
selected_label: Option<SharedString>,
selected_label_color: Option<Color>,
start_icon: Option<Icon>,
end_icon: Option<Icon>,
key_binding: Option<KeyBinding>,
key_binding_position: KeybindingPosition,
alpha: Option<f32>,
truncate: bool,
loading: bool,
}
impl Button {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
base: ButtonLike::new(id),
label: label.into(),
label_color: None,
label_size: None,
selected_label: None,
selected_label_color: None,
start_icon: None,
end_icon: None,
key_binding: None,
key_binding_position: KeybindingPosition::default(),
alpha: None,
truncate: false,
loading: false,
}
}
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
self.label_color = label_color.into();
self
}
pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
self.label_size = label_size.into();
self
}
pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
self.selected_label = label.into().map(Into::into);
self
}
pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
self.selected_label_color = color.into();
self
}
pub fn start_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
self.start_icon = icon.into();
self
}
pub fn end_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
self.end_icon = icon.into();
self
}
pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
self.key_binding = key_binding.into();
self
}
pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
self.key_binding_position = position;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = Some(alpha);
self
}
pub fn truncate(mut self, truncate: bool) -> Self {
self.truncate = truncate;
self
}
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
pub fn primary(mut self) -> Self {
self.base = self.base.style(ButtonStyle::Tinted(TintColor::Accent));
self.label_color = Some(Color::Custom(gpui::white()));
self
}
pub fn danger(mut self) -> Self {
self.base = self.base.style(ButtonStyle::Tinted(TintColor::Error));
self.label_color = Some(Color::Custom(gpui::white()));
self
}
pub fn soft(mut self) -> Self {
self.base = self
.base
.style(ButtonStyle::Tinted(TintColor::Accent))
.background_override(palette::primary(50));
self.label_color = Some(Color::Custom(palette::primary(700)));
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
match variant {
ButtonVariant::Default => self.primary(),
ButtonVariant::Destructive => self.danger(),
ButtonVariant::Outline => {
self.base = self.base.style(ButtonStyle::Outlined);
self.label_color = None;
self
}
ButtonVariant::Secondary => {
self.base = self.base.style(ButtonStyle::Filled);
self.label_color = None;
self
}
ButtonVariant::Ghost => {
self.base = self.base.style(ButtonStyle::Transparent);
self.label_color = None;
self
}
}
}
pub fn shadcn_size(mut self, size: ButtonSizeAlias) -> Self {
let mapped = match size {
ButtonSizeAlias::Sm => ButtonSize::Compact,
ButtonSizeAlias::Default => ButtonSize::Default,
ButtonSizeAlias::Lg => ButtonSize::Medium,
ButtonSizeAlias::Icon => ButtonSize::None,
};
self.base = self.base.size(mapped);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonVariant {
#[default]
Default,
Destructive,
Outline,
Secondary,
Ghost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonSizeAlias {
Sm,
#[default]
Default,
Lg,
Icon,
}
impl Toggleable for Button {
fn toggle_state(mut self, selected: bool) -> Self {
self.base = self.base.toggle_state(selected);
self
}
}
impl SelectableButton for Button {
fn selected_style(mut self, style: ButtonStyle) -> Self {
self.base = self.base.selected_style(style);
self
}
}
impl Disableable for Button {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self
}
}
impl Clickable for Button {
fn on_click(
mut self,
handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.base = self.base.on_click(handler);
self
}
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
self.base = self.base.cursor_style(cursor_style);
self
}
}
impl FixedWidth for Button {
fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
self.base = self.base.width(width);
self
}
fn full_width(mut self) -> Self {
self.base = self.base.full_width();
self
}
}
impl ButtonCommon for Button {
fn id(&self) -> &ElementId {
self.base.id()
}
fn style(mut self, style: ButtonStyle) -> Self {
self.base = self.base.style(style);
self
}
fn size(mut self, size: ButtonSize) -> Self {
self.base = self.base.size(size);
self
}
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
self.base = self.base.tooltip(tooltip);
self
}
fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
self.base = self.base.tab_index(tab_index);
self
}
fn layer(mut self, elevation: ElevationIndex) -> Self {
self.base = self.base.layer(elevation);
self
}
fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
self.base = self.base.track_focus(focus_handle);
self
}
}
impl RenderOnce for Button {
#[allow(refining_impl_trait)]
fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike {
let is_disabled = self.base.disabled;
let is_selected = self.base.selected;
let button_style = self.base.style;
let label = self
.selected_label
.filter(|_| is_selected)
.unwrap_or(self.label);
let label_color = if is_disabled {
Color::Disabled
} else if is_selected {
self.selected_label_color.unwrap_or(Color::Selected)
} else if let Some(color) = self.label_color {
color
} else if matches!(button_style, ButtonStyle::Tinted(_)) {
Color::Custom(gpui::white())
} else {
Color::default()
};
self.base.child(
h_flex()
.when(self.truncate, |this| this.min_w_0().overflow_hidden())
.gap(DynamicSpacing::Base04.rems(cx))
.when(self.loading, |this| {
this.child(
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2),
)
})
.when(!self.loading, |this| {
this.when_some(self.start_icon, |this, icon| {
this.child(if is_disabled {
icon.color(Color::Disabled)
} else if matches!(button_style, ButtonStyle::Tinted(_)) {
icon.color(Color::Custom(gpui::white()))
} else {
icon
})
})
})
.child(
h_flex()
.when(self.truncate, |this| this.min_w_0().overflow_hidden())
.when(
self.key_binding_position == KeybindingPosition::Start,
|this| this.flex_row_reverse(),
)
.gap(DynamicSpacing::Base06.rems(cx))
.justify_between()
.child(
Label::new(label)
.color(label_color)
.size(self.label_size.unwrap_or_default())
.when_some(self.alpha, |this, alpha| this.alpha(alpha))
.when(self.truncate, |this| this.truncate()),
)
.children(self.key_binding),
)
.when_some(self.end_icon, |this, icon| {
this.child(if is_disabled {
icon.color(Color::Disabled)
} else {
icon
})
}),
)
}
}
impl Component for Button {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
"ButtonA"
}
fn description() -> Option<&'static str> {
Some("A button triggers an event or action.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Button Styles",
vec![
single_example(
"Default",
Button::new("default", "Default").into_any_element(),
),
single_example(
"Filled",
Button::new("filled", "Filled")
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Subtle",
Button::new("outline", "Subtle")
.style(ButtonStyle::Subtle)
.into_any_element(),
),
single_example(
"Tinted",
Button::new("tinted_accent_style", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Transparent",
Button::new("transparent", "Transparent")
.style(ButtonStyle::Transparent)
.into_any_element(),
),
],
),
example_group_with_title(
"Primary / Danger / Soft",
vec![
single_example(
"Primary",
Button::new("primary", "Primary")
.primary()
.into_any_element(),
),
single_example(
"Danger",
Button::new("danger", "Danger").danger().into_any_element(),
),
single_example(
"Soft",
Button::new("soft", "Soft").soft().into_any_element(),
),
],
),
example_group_with_title(
"Variant \u{d7} Size Matrix",
vec![
single_example(
"Large",
h_flex()
.gap_2()
.child(
Button::new("matrix_primary_lg", "Primary")
.primary()
.size(ButtonSize::Large),
)
.child(
Button::new("matrix_danger_lg", "Danger")
.danger()
.size(ButtonSize::Large),
)
.child(
Button::new("matrix_soft_lg", "Soft")
.soft()
.size(ButtonSize::Large),
)
.into_any_element(),
),
single_example(
"Medium",
h_flex()
.gap_2()
.child(
Button::new("matrix_primary_md", "Primary")
.primary()
.size(ButtonSize::Medium),
)
.child(
Button::new("matrix_danger_md", "Danger")
.danger()
.size(ButtonSize::Medium),
)
.child(
Button::new("matrix_soft_md", "Soft")
.soft()
.size(ButtonSize::Medium),
)
.into_any_element(),
),
single_example(
"Compact",
h_flex()
.gap_2()
.child(
Button::new("matrix_primary_cp", "Primary")
.primary()
.size(ButtonSize::Compact),
)
.child(
Button::new("matrix_danger_cp", "Danger")
.danger()
.size(ButtonSize::Compact),
)
.child(
Button::new("matrix_soft_cp", "Soft")
.soft()
.size(ButtonSize::Compact),
)
.into_any_element(),
),
],
),
example_group_with_title(
"Tint Styles",
vec![
single_example(
"Accent",
Button::new("tinted_accent", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Error",
Button::new("tinted_negative", "Error")
.style(ButtonStyle::Tinted(TintColor::Error))
.into_any_element(),
),
single_example(
"Warning",
Button::new("tinted_warning", "Warning")
.style(ButtonStyle::Tinted(TintColor::Warning))
.into_any_element(),
),
single_example(
"Success",
Button::new("tinted_positive", "Success")
.style(ButtonStyle::Tinted(TintColor::Success))
.into_any_element(),
),
],
),
example_group_with_title(
"Special States",
vec![
single_example(
"Default",
Button::new("default_state", "Default").into_any_element(),
),
single_example(
"Disabled",
Button::new("disabled", "Disabled")
.disabled(true)
.into_any_element(),
),
single_example(
"Selected",
Button::new("selected", "Selected")
.toggle_state(true)
.into_any_element(),
),
],
),
example_group_with_title(
"Buttons with Icons",
vec![
single_example(
"Start Icon",
Button::new("icon_start", "Start Icon")
.start_icon(Icon::new(IconName::Check))
.into_any_element(),
),
single_example(
"End Icon",
Button::new("icon_end", "End Icon")
.end_icon(Icon::new(IconName::Check))
.into_any_element(),
),
single_example(
"Both Icons",
Button::new("both_icons", "Both Icons")
.start_icon(Icon::new(IconName::Check))
.end_icon(Icon::new(IconName::ChevronDown))
.into_any_element(),
),
single_example(
"Icon Color",
Button::new("icon_color", "Icon Color")
.start_icon(Icon::new(IconName::Check).color(Color::Accent))
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}