button.rs

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