;
/// A MetalUI button: a pill-shaped cap that sinks into a well when pressed.
#[derive(IntoElement)]
pub struct MetalButton {
base: Stateful,
kind: CapKind,
size: CapSize,
icon: Option,
icon_color: Option,
label: Option,
caret: bool,
stretch: bool,
selected: bool,
disabled: bool,
tooltip: Option,
on_click: Option,
}
impl MetalButton {
pub fn new(id: impl Into) -> Self {
Self {
base: div().id(id),
kind: CapKind::Standard,
size: CapSize::Regular,
icon: None,
icon_color: None,
label: None,
caret: false,
stretch: false,
selected: false,
disabled: false,
tooltip: None,
on_click: None,
}
}
pub fn kind(mut self, kind: CapKind) -> Self {
self.kind = kind;
self
}
pub fn primary(self) -> Self {
self.kind(CapKind::Primary)
}
pub fn danger(self) -> Self {
self.kind(CapKind::Danger)
}
pub fn ghost(self) -> Self {
self.kind(CapKind::Ghost)
}
pub fn compact(mut self) -> Self {
self.size = CapSize::Compact;
self
}
pub fn mini(mut self) -> Self {
self.size = CapSize::Mini;
self
}
pub fn icon(mut self, icon: impl Into) -> Self {
self.icon = Some(icon.into());
self
}
pub fn icon_color(mut self, color: Hsla) -> Self {
self.icon_color = Some(color);
self
}
pub fn label(mut self, label: impl Into) -> Self {
self.label = Some(label.into());
self
}
pub fn caret(mut self) -> Self {
self.caret = true;
self
}
/// Lets the label take the spare width, pushing the caret to the far edge.
pub fn stretch(mut self) -> Self {
self.stretch = true;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn tooltip(mut self, tooltip: impl Into) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
}
impl Styled for MetalButton {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl InteractiveElement for MetalButton {
fn interactivity(&mut self) -> &mut Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for MetalButton {}
impl DropdownMenu for MetalButton {}
impl Selectable for MetalButton {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
fn is_selected(&self) -> bool {
self.selected
}
}
impl RenderOnce for MetalButton {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let p = palette(cx);
let mut base = self.base;
let instance_style = base.style().clone();
let icon_only = self.label.is_none();
let height = self.size.height();
let (bg, pressed_bg, shadow, pressed_shadow, ink) = match self.kind {
CapKind::Standard | CapKind::Ghost => {
(vertical(p.cap), vertical(p.pressed), p.btn.clone(), p.pressed_sh.clone(), p.ink)
}
CapKind::Primary => (
vertical(p.primary),
vertical(p.primary_pressed),
p.primary_sh.clone(),
p.primary_pressed_sh.clone(),
p.primary_ink,
),
CapKind::Danger => (
vertical(p.danger),
vertical(p.danger_pressed),
p.danger_sh.clone(),
p.danger_pressed_sh.clone(),
white(),
),
};
let ghost = self.kind == CapKind::Ghost;
let interactive = !self.disabled;
base.flex()
.flex_none()
.items_center()
.justify_center()
.gap(px(6.))
.h(height)
.map(|this| if icon_only { this.w(height) } else { this.px(self.size.pad()) })
.rounded_full()
.text_size(self.size.font())
.font_weight(FontWeight::MEDIUM)
.line_height(px(16.))
.text_color(if ghost { p.ink2 } else { ink })
.when(!ghost, |this| this.bg(bg).shadow(shadow))
.when(interactive, |this| {
this.cursor_pointer()
.when(ghost, |this| this.hover(|style| style.bg(p.row_hover).text_color(p.ink)))
.active(|style| style.bg(pressed_bg).shadow(pressed_shadow.clone()))
})
.when(self.selected, |this| {
this.bg(vertical(p.pressed)).shadow(p.pressed_sh.clone()).text_color(p.ink)
})
.when(self.disabled, |this| this.opacity(0.4))
.refine_style(&instance_style)
.when_some(self.icon, |this, icon| {
let icon = icon.size(self.size.glyph());
this.child(match self.icon_color {
Some(color) => icon.text_color(color),
None => icon,
})
})
.when_some(self.label, |this, label| {
this.child(
div()
.whitespace_nowrap()
.when(self.stretch, |this| this.flex_1().min_w_0().truncate())
.child(label),
)
})
.when(self.caret, |this| {
this.child(
Icon::new(IconName::ChevronDown)
.xsmall()
.text_color(if ghost { p.ink3 } else { ink.opacity(0.6) }),
)
})
.when_some(self.tooltip.filter(|_| interactive), |this, tooltip| {
this.tooltip(move |window, cx| Tooltip::new(tooltip.clone()).build(window, cx))
})
.when_some(self.on_click.filter(|_| interactive), |this, handler| {
this.on_click(move |event, window, cx| handler(event, window, cx))
})
}
}
#[cfg(test)]
mod tests {
// Not a glob: GPUI exports its own `test` attribute.
use super::{BONE, GRAPHITE, hex, shadows};
use gpui_kit::px;
#[test]
fn palettes_parse() {
assert!(!BONE.raise.is_empty());
assert!(!GRAPHITE.raise.is_empty());
}
#[test]
fn shadow_layers() {
let layers = shadows("inset 0 0 0 .5px #0009, 0 1px 2px #00000038");
assert_eq!(layers.len(), 2);
assert!(layers[0].inset);
assert_eq!(layers[0].spread_radius, px(0.5));
assert!(!layers[1].inset);
assert_eq!(layers[1].blur_radius, px(2.));
}
#[test]
fn short_hex() {
assert_eq!(hex("#fff"), hex("#ffffffff"));
assert_eq!(hex("#0009"), hex("#00000099"));
}
}