button.rs

  1use std::rc::Rc;
  2
  3use gpui::{
  4    DefiniteLength, Div, Hsla, MouseButton, MouseDownEvent, RenderOnce, StatefulInteractiveElement,
  5    WindowContext,
  6};
  7
  8use crate::prelude::*;
  9use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
 10
 11/// Provides the flexibility to use either a standard
 12/// button or an icon button in a given context.
 13pub enum ButtonOrIconButton {
 14    Button(Button),
 15    IconButton(IconButton),
 16}
 17
 18impl From<Button> for ButtonOrIconButton {
 19    fn from(value: Button) -> Self {
 20        Self::Button(value)
 21    }
 22}
 23
 24impl From<IconButton> for ButtonOrIconButton {
 25    fn from(value: IconButton) -> Self {
 26        Self::IconButton(value)
 27    }
 28}
 29
 30#[derive(Default, PartialEq, Clone, Copy)]
 31pub enum IconPosition {
 32    #[default]
 33    Left,
 34    Right,
 35}
 36
 37#[derive(Default, Copy, Clone, PartialEq)]
 38pub enum ButtonVariant {
 39    #[default]
 40    Ghost,
 41    Filled,
 42}
 43
 44impl ButtonVariant {
 45    pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
 46        match self {
 47            ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
 48            ButtonVariant::Filled => cx.theme().colors().element_background,
 49        }
 50    }
 51
 52    pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
 53        match self {
 54            ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
 55            ButtonVariant::Filled => cx.theme().colors().element_hover,
 56        }
 57    }
 58
 59    pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
 60        match self {
 61            ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
 62            ButtonVariant::Filled => cx.theme().colors().element_active,
 63        }
 64    }
 65}
 66
 67#[derive(RenderOnce)]
 68pub struct Button {
 69    disabled: bool,
 70    click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
 71    icon: Option<Icon>,
 72    icon_position: Option<IconPosition>,
 73    label: SharedString,
 74    variant: ButtonVariant,
 75    width: Option<DefiniteLength>,
 76    color: Option<TextColor>,
 77}
 78
 79impl Component for Button {
 80    type Rendered = gpui::Stateful<Div>;
 81
 82    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
 83        let (icon_color, label_color) = match (self.disabled, self.color) {
 84            (true, _) => (TextColor::Disabled, TextColor::Disabled),
 85            (_, None) => (TextColor::Default, TextColor::Default),
 86            (_, Some(color)) => (TextColor::from(color), color),
 87        };
 88
 89        let mut button = h_stack()
 90            .id(SharedString::from(format!("{}", self.label)))
 91            .relative()
 92            .p_1()
 93            .text_ui()
 94            .rounded_md()
 95            .bg(self.variant.bg_color(cx))
 96            .cursor_pointer()
 97            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
 98            .active(|style| style.bg(self.variant.bg_color_active(cx)));
 99
100        match (self.icon, self.icon_position) {
101            (Some(_), Some(IconPosition::Left)) => {
102                button = button
103                    .gap_1()
104                    .child(self.render_label(label_color))
105                    .children(self.render_icon(icon_color))
106            }
107            (Some(_), Some(IconPosition::Right)) => {
108                button = button
109                    .gap_1()
110                    .children(self.render_icon(icon_color))
111                    .child(self.render_label(label_color))
112            }
113            (_, _) => button = button.child(self.render_label(label_color)),
114        }
115
116        if let Some(width) = self.width {
117            button = button.w(width).justify_center();
118        }
119
120        if let Some(click_handler) = self.click_handler.clone() {
121            button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
122                click_handler(event, cx);
123            });
124        }
125
126        button
127    }
128}
129
130impl Button {
131    pub fn new(label: impl Into<SharedString>) -> Self {
132        Self {
133            disabled: false,
134            click_handler: None,
135            icon: None,
136            icon_position: None,
137            label: label.into(),
138            variant: Default::default(),
139            width: Default::default(),
140            color: None,
141        }
142    }
143
144    pub fn ghost(label: impl Into<SharedString>) -> Self {
145        Self::new(label).variant(ButtonVariant::Ghost)
146    }
147
148    pub fn variant(mut self, variant: ButtonVariant) -> Self {
149        self.variant = variant;
150        self
151    }
152
153    pub fn icon(mut self, icon: Icon) -> Self {
154        self.icon = Some(icon);
155        self
156    }
157
158    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
159        if self.icon.is_none() {
160            panic!("An icon must be present if an icon_position is provided.");
161        }
162        self.icon_position = Some(icon_position);
163        self
164    }
165
166    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
167        self.width = width;
168        self
169    }
170
171    pub fn on_click(
172        mut self,
173        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
174    ) -> Self {
175        self.click_handler = Some(Rc::new(handler));
176        self
177    }
178
179    pub fn disabled(mut self, disabled: bool) -> Self {
180        self.disabled = disabled;
181        self
182    }
183
184    pub fn color(mut self, color: Option<TextColor>) -> Self {
185        self.color = color;
186        self
187    }
188
189    pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
190        if self.disabled {
191            TextColor::Disabled
192        } else if let Some(color) = color {
193            color
194        } else {
195            Default::default()
196        }
197    }
198
199    fn render_label(&self, color: TextColor) -> Label {
200        Label::new(self.label.clone())
201            .color(color)
202            .line_height_style(LineHeightStyle::UILabel)
203    }
204
205    fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
206        self.icon.map(|i| IconElement::new(i).color(icon_color))
207    }
208}
209
210#[derive(RenderOnce)]
211pub struct ButtonGroup {
212    buttons: Vec<Button>,
213}
214
215impl Component for ButtonGroup {
216    type Rendered = Div;
217
218    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
219        let mut group = h_stack();
220
221        for button in self.buttons.into_iter() {
222            group = group.child(button.render(cx));
223        }
224
225        group
226    }
227}
228
229impl ButtonGroup {
230    pub fn new(buttons: Vec<Button>) -> Self {
231        Self { buttons }
232    }
233}