use gpui::{Div, ElementId, SharedString, Stateful, div, prelude::*, px};
use icons::Icon;
use motion::{self, Fade};
use theme::{ControlSize, Sizing, Theme, ThemeExt, Typeset as _};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonStyle {
Ghost,
Prominent,
Destructive,
}
pub const ICON_GLYPH: f32 = 14.0;
pub const ICON_EDGE_PAD: f32 = Theme::EDGE - (Theme::BUTTON_HEIGHT - ICON_GLYPH) / 2.0;
const GROUP_PAD: f32 = 2.0;
fn frame() -> Div {
div()
.control_size(ControlSize::Regular)
.flex()
.items_center()
.cursor_pointer()
}
fn square() -> Div {
frame()
.px(px(0.0))
.w(px(Theme::BUTTON_HEIGHT))
.justify_center()
}
pub trait Buttons: ThemeExt {
fn button(
&self,
label: impl Into<SharedString>,
style: ButtonStyle,
fade: Option<Fade>,
) -> Div {
let (button, _) = appearance(self.theme(), frame(), style, None, fade, true);
button.child(label.into())
}
fn icon_button(&self, icon: impl Into<Icon>, style: ButtonStyle, fade: Option<Fade>) -> Div {
let (button, tint) = appearance(self.theme(), square(), style, None, fade, true);
button.child(
crate::icons::icon(icon)
.size(px(ICON_GLYPH))
.text_color(tint),
)
}
fn tinted_icon_button(&self, icon: impl Into<Icon>, tint: gpui::Hsla) -> Div {
square().text_color(tint).child(
crate::icons::icon(icon)
.size(px(ICON_GLYPH))
.text_color(tint),
)
}
fn control_group(&self) -> Div {
let theme = self.theme();
div()
.self_start()
.flex()
.flex_row()
.gap(px(GROUP_PAD))
.p(px(GROUP_PAD))
.rounded(px(Theme::button_radius() + GROUP_PAD))
.bg(theme.surface_raised)
.border_1()
.border_color(theme.border)
}
fn split_button(
&self,
main: impl Into<ElementId>,
more: impl Into<ElementId>,
icon: impl IntoElement,
label: impl Into<SharedString>,
size: ControlSize,
) -> SplitButton {
let theme = self.theme();
let pad = size.pad_x();
let main = div()
.id(main)
.flex()
.items_center()
.gap(px(pad / 2.0 + 1.0))
.pl(px(pad / 2.0 + 1.0))
.pr(px(pad))
.cursor_pointer()
.hover(|el| el.bg(theme.element_hover))
.child(icon)
.child(label.into());
let more = div()
.id(more)
.flex()
.items_center()
.px(px(pad / 2.0))
.border_l_1()
.border_color(theme.border)
.cursor_pointer()
.hover(|el| el.bg(theme.element_hover))
.child(
crate::icons::icon(crate::icons::arrows::ChevronDown)
.size(px(12.0))
.text_color(theme.text_muted),
);
SplitButton {
main,
more,
size,
border: theme.border,
fill: theme.surface_raised,
text: theme.text,
}
}
fn ghost(&self, id: impl Into<ElementId>) -> Stateful<Div> {
let tint = self.theme().element_hover;
div()
.id(id)
.flex()
.flex_row()
.items_center()
.rounded(px(Theme::control_radius()))
.cursor_pointer()
.hover(move |el| el.bg(tint))
}
}
impl Buttons for Theme {}
pub struct SplitButton {
pub main: Stateful<Div>,
pub more: Stateful<Div>,
size: ControlSize,
border: gpui::Hsla,
fill: gpui::Hsla,
text: gpui::Hsla,
}
impl SplitButton {
pub fn build(self, menu: Option<gpui::AnyElement>) -> Div {
let radius = px(self.size.radius());
self.frame()
.child(self.main.rounded_l(radius))
.child(self.more.rounded_r(radius))
.children(menu)
}
pub fn build_alone(self) -> Div {
let radius = px(self.size.radius());
self.frame().child(self.main.rounded(radius))
}
fn frame(&self) -> Div {
div()
.relative()
.flex_none()
.flex()
.items_stretch()
.h(px(self.size.height()))
.rounded(px(self.size.radius()))
.border_1()
.border_color(self.border)
.bg(self.fill)
.text_style(self.size.text())
.text_color(self.text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonRole {
Cancel,
Destructive,
}
pub(super) fn appearance(
theme: &Theme,
frame: Div,
style: ButtonStyle,
role: Option<ButtonRole>,
fade: Option<Fade>,
interactive: bool,
) -> (Div, gpui::Hsla) {
let destructive = role == Some(ButtonRole::Destructive) || style == ButtonStyle::Destructive;
if style == ButtonStyle::Ghost {
let rest = if destructive {
theme.danger_strong
} else {
theme.text_muted
};
let hot = if destructive {
theme.danger_strong
} else {
theme.text
};
if let Some(fade) = fade {
let tint = motion::hover_blend(&fade, rest, hot);
let mut button = frame.text_color(tint).bg(motion::hover_blend(
&fade,
theme.ink(0.0),
theme.element_hover,
));
button
.interactivity()
.on_hover(motion::hover_listener(fade));
(button, tint)
} else {
(frame.text_color(rest), rest)
}
} else {
let (fill, tint) = if destructive {
(theme.danger_strong, gpui::white())
} else {
(theme.text, theme.on_solid)
};
(
frame
.bg(fill)
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(tint)
.when(interactive, |button| button.hover(|s| s.opacity(0.9))),
tint,
)
}
}