{
div()
.id(id)
.flex()
.flex_none()
.items_center()
.when(on, |this| this.justify_end())
.w(px(38.))
.h(px(22.))
.p(px(3.))
.rounded_full()
.cursor_pointer()
.map(|this| {
if on {
this.bg(vertical((p.green_ink, p.green_deep))).shadow(p.pressed_sh.clone())
} else {
this.well(p)
}
})
.on_click(move |_, window, cx| on_toggle(!on, window, cx))
.child(div().size(px(16.)).rounded_full().bg(vertical(p.cap)).shadow(p.raise_sm.clone()))
}
/// A segmented switch: options as caps in one well, the current one raised.
pub fn segmented
(
id: &'static str,
options: &[(T, &'static str)],
current: T,
p: &Palette,
on_pick: impl Fn(T, &mut Window, &mut App) + 'static,
) -> Div {
let on_pick = Rc::new(on_pick);
h_flex()
.flex_none()
.p(px(3.))
.gap(px(2.))
.rounded_full()
.well(p)
.children(options.iter().enumerate().map(|(ix, &(value, label))| {
let selected = value == current;
let on_pick = on_pick.clone();
div()
.id((id, ix))
.flex()
.items_center()
.h(px(24.))
.px(px(12.))
.rounded_full()
.text_size(px(12.5))
.font_weight(FontWeight::MEDIUM)
.map(|this| {
if selected {
this.bg(vertical(p.cap)).shadow(p.btn.clone()).text_color(p.ink)
} else {
this.cursor_pointer().text_color(p.ink3).hover(|style| style.text_color(p.ink))
}
})
.on_click(move |_, window, cx| on_pick(value, window, cx))
.child(label)
}))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Led {
Live,
Waiting,
Failed,
Off,
}
/// A small domed status light. `pulse` breathes it, for work in progress.
pub fn led(id: impl Into, kind: Led, pulse: bool, p: &Palette) -> AnyElement {
let (top, bottom, glow) = match kind {
Led::Live => (rgb(0xd9ffe9).into(), rgb(0x2fb673).into(), Some(rgba(0x7beaae8c).into())),
Led::Waiting => (rgb(0xfff1cf).into(), rgb(0xc98a18).into(), Some(rgba(0xf5bf558c).into())),
Led::Failed => (rgb(0xffd9d2).into(), rgb(0xd5392a).into(), Some(rgba(0xff7a668c).into())),
Led::Off => (p.led_off.0, p.led_off.1, None),
};
let light = div()
.flex_none()
.size(px(6.))
.rounded_full()
.bg(linear_gradient(
150.,
linear_color_stop(top, 0.),
linear_color_stop(bottom, 0.75),
))
.shadow(match glow {
Some(glow) => vec![
BoxShadow::new(px(0.), px(0.), rgba(0x00000040).into()).spread_radius(px(0.5)),
BoxShadow::new(px(0.), px(0.), glow).blur_radius(px(3.)),
],
None => p.led_off_sh.clone(),
});
if pulse {
light
.with_animation(
id,
Animation::new(Duration::from_millis(1600))
.repeat()
.with_easing(pulsating_between(0.35, 1.)),
|light, delta| light.opacity(delta),
)
.into_any_element()
} else {
light.into_any_element()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CapKind {
/// The everyday raised button.
Standard,
/// The one action that matters: an inverted cap.
Primary,
Danger,
/// No cap until the pointer is over it.
Ghost,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CapSize {
/// 32px, MetalUI's default button.
Regular,
/// 26px, for toolbars.
Compact,
/// 22px, for icon buttons tucked into rows.
Mini,
}
impl CapSize {
fn height(self) -> Pixels {
match self {
CapSize::Regular => px(32.),
CapSize::Compact => px(26.),
CapSize::Mini => px(22.),
}
}
fn pad(self) -> Pixels {
match self {
CapSize::Regular => px(15.),
CapSize::Compact => px(11.),
CapSize::Mini => px(8.),
}
}
fn font(self) -> Pixels {
match self {
CapSize::Regular => px(13.),
CapSize::Compact | CapSize::Mini => px(12.),
}
}
fn glyph(self) -> Pixels {
match self {
CapSize::Regular => px(16.),
CapSize::Compact => px(14.),
CapSize::Mini => px(13.),
}
}
}
type ClickHandler = Rc;
/// 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,
/// Drawn where the icon goes, such as a project's badge.
leading: 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,
leading: 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
}
/// Something other than an icon before the label.
pub fn leading(mut self, element: impl IntoElement) -> Self {
self.leading = Some(element.into_any_element());
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,
})
})
.children(self.leading)
.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"));
}
}