use gpui::{ div, prelude::FluentBuilder as _, ClickEvent, DefiniteLength, Div, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, }; use crate::{ disableable::{Clickable, Disableable, Selectable}, label::Label, theme::{Colorize as _, Theme}, }; pub enum ButtonRounded { None, Small, Medium, Large, } pub enum ButtonSize { Small, Medium, } pub enum ButtonStyle { Primary, Secondary, Danger, } #[derive(IntoElement)] pub struct Button { pub base: Div, id: ElementId, label: SharedString, disabled: bool, selected: bool, width: Option, height: Option, style: ButtonStyle, rounded: ButtonRounded, size: ButtonSize, tooltip: Option, on_click: Option>, } impl Button { pub fn new(id: impl Into, label: impl Into) -> Self { Self { base: div(), id: id.into(), label: label.into(), disabled: false, selected: false, style: ButtonStyle::Secondary, width: None, height: None, rounded: ButtonRounded::Medium, size: ButtonSize::Medium, tooltip: None, on_click: None, } } pub fn width(mut self, width: impl Into) -> Self { self.width = Some(width.into()); self } pub fn height(mut self, height: impl Into) -> Self { self.height = Some(height.into()); self } pub fn rounded(mut self, rounded: ButtonRounded) -> Self { self.rounded = rounded; self } pub fn size(mut self, size: ButtonSize) -> Self { self.size = size; self } pub fn tooltip(mut self, tooltip: impl Into) -> Self { self.tooltip = Some(tooltip.into()); self } pub fn style(mut self, style: ButtonStyle) -> Self { self.style = style; self } } impl Disableable for Button { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl Selectable for Button { fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } } impl Clickable for Button { fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self } } impl RenderOnce for Button { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let theme = cx.global::(); let style: ButtonStyle = self.style; let normal_style = style.normal(cx); self.base .id(self.id) .flex() .items_center() .justify_center() .map(|this| match self.size { ButtonSize::Small => this.px_3().py_2().h_6(), ButtonSize::Medium => this.px_4().py_2().h_8(), }) .map(|this| match self.rounded { ButtonRounded::Small => this.rounded_sm(), ButtonRounded::Medium => this.rounded_md(), ButtonRounded::Large => this.rounded_lg(), ButtonRounded::None => this.rounded_none(), }) .when(!self.disabled, |this| { this.hover(|this| { let hover_style = style.hovered(cx); this.bg(hover_style.bg).border_color(hover_style.border) }) .active(|this| { let active_style = style.active(cx); this.bg(active_style.bg).border_color(active_style.border) }) }) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default()) .on_click(move |event, cx| { cx.stop_propagation(); (on_click)(event, cx) }) }, ) .when(self.disabled, |this| { let disabled_style = style.disabled(cx); this.cursor_not_allowed() .bg(disabled_style.bg) .border_color(disabled_style.border) }) .border_1() .border_color(normal_style.border) .bg(normal_style.bg) .child({ let text_color = if self.disabled { normal_style.fg.opacity(0.6) } else { normal_style.fg }; Label::new(self.label) .color(text_color) .map(|this| match self.size { ButtonSize::Small => this.text_sm(), ButtonSize::Medium => this.text_base(), }) }) } } struct ButtonStyles { bg: Hsla, border: Hsla, fg: Hsla, } impl ButtonStyle { fn bg_color(&self, cx: &WindowContext) -> Hsla { let theme = cx.global::(); match self { ButtonStyle::Primary => theme.primary, ButtonStyle::Secondary => theme.secondary, ButtonStyle::Danger => theme.destructive, } } fn text_color(&self, cx: &WindowContext) -> Hsla { let theme = cx.global::(); match self { ButtonStyle::Primary => theme.primary_foreground, ButtonStyle::Secondary => theme.secondary_foreground, ButtonStyle::Danger => theme.destructive_foreground, } } fn border_color(&self, cx: &WindowContext) -> Hsla { let theme = cx.global::(); match self { ButtonStyle::Primary => theme.primary, ButtonStyle::Secondary => theme.secondary, ButtonStyle::Danger => theme.destructive, } } fn normal(&self, cx: &WindowContext) -> ButtonStyles { let bg = self.bg_color(cx); let border = self.border_color(cx); let fg = self.text_color(cx); ButtonStyles { bg, border, fg } } fn hovered(&self, cx: &WindowContext) -> ButtonStyles { // Hover color = color/90 let bg = self.bg_color(cx).divide(0.9); let border = self.border_color(cx).divide(0.9); let fg = self.text_color(cx).divide(0.9); ButtonStyles { bg, border, fg } } fn active(&self, cx: &WindowContext) -> ButtonStyles { let bg = self.bg_color(cx); let border = self.border_color(cx); let fg = self.text_color(cx); ButtonStyles { bg, border, fg } } fn disabled(&self, cx: &WindowContext) -> ButtonStyles { let bg = self.bg_color(cx).grayscale(); let border = self.border_color(cx).grayscale(); let fg = self.text_color(cx).grayscale(); ButtonStyles { bg, border, fg } } }