//! The push button: the API template for every control that follows. use gpui::prelude::FluentBuilder as _; use gpui::{ App, BoxShadow, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, px, relative, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, Size}; use crate::common::{Sizable, h_flex}; /// The single activation handler, shared by pointer, Enter, and Space. type ClickHandler = Box; /// Which visual treatment a button uses. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ButtonVariant { /// Accent-filled; the platform's default action. #[default] Primary, /// Neutral and bordered; the everyday companion to `Primary`. Secondary, /// Unpainted until hovered, for toolbars and dense UI. Ghost, /// The destructive action. Destructive, } /// A push button: pointer, Enter, and Space all fire the single `on_click`. #[derive(IntoElement)] pub struct Button { id: ElementId, label: Option, icon: Option, variant: ButtonVariant, size: Size, disabled: bool, a11y_label: Option, on_click: Option, } impl Button { /// Creates a primary button. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), label: None, icon: None, variant: ButtonVariant::default(), size: Size::default(), disabled: false, a11y_label: None, on_click: None, } } /// Sets the visible label. pub fn label(mut self, label: L) -> Self where L: Into, { self.label = Some(label.into()); self } /// Shows an icon beside the label. pub fn icon(mut self, role: IconRole) -> Self { self.icon = Some(role); self } /// Sets the visual treatment. pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; self } /// Renders the button inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label, which defaults to the visible label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } /// The single activation handler, fired by pointer, Enter, and Space. pub fn on_click(mut self, handler: F) -> Self where F: Fn(&ClickEvent, &mut Window, &mut App) + 'static, { self.on_click = Some(Box::new(handler)); self } } impl Sizable for Button { fn apply_size(&mut self, size: Size) { self.size = size; } } impl RenderOnce for Button { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let ramps = theme.ramps; let metrics = theme.sizes.resolve(self.size); let on_click = self.on_click; let a11y_label = self.a11y_label.or_else(|| self.label.clone()); let (background, foreground, hover, pressed) = match self.variant { ButtonVariant::Primary => ( Some(ramps.primary.base), colors.primary_foreground, ramps.primary.hover, ramps.primary.pressed, ), ButtonVariant::Secondary => ( Some(colors.secondary), colors.secondary_foreground, ramps.secondary.hover, ramps.secondary.pressed, ), ButtonVariant::Ghost => ( None, colors.foreground, colors.muted, ramps.secondary.pressed, ), ButtonVariant::Destructive => ( Some(ramps.destructive.base), colors.destructive_foreground, ramps.destructive.hover, ramps.destructive.pressed, ), }; h_flex() .id(self.id) .role(Role::Button) .justify_center() .h(metrics.control_height) .px(metrics.padding_x) .line_height(relative(1.0)) .gap_1p5() .rounded(theme.radius.control) .text_size(metrics.text_size) .when(self.variant == ButtonVariant::Secondary, |button| { button.border_1().border_color(colors.border) }) .when_some(background, |button, background| button.bg(background)) .text_color(foreground) .when_some(a11y_label, |button, label| button.aria_label(label)) .when_some(self.icon, |button, role| { button.child( icon(role) .size(f32::from(metrics.icon_size)) .color(foreground), ) }) .when_some(self.label, |button, label| button.child(label)) .when(self.disabled, |button| button.opacity(0.5)) .when(!self.disabled, |button| { button .cursor_pointer() .focusable() .tab_stop(true) .hover(move |style| style.bg(hover)) .active(move |style| style.bg(pressed)) .focus_visible(|style| { // Paint-only keyboard ring; the width joins the native // metrics table when that lands. style.shadow(vec![ BoxShadow::new(px(0.), px(0.), colors.ring).spread_radius(px(3.)), ]) }) .when_some(on_click, |button, on_click| { button.on_click(move |event, window, cx| on_click(event, window, cx)) }) }) } }