//! The push button: the API template for every control that follows. use gpui::prelude::FluentBuilder as _; use gpui::{ AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, relative, transparent_black, }; use gpui_native_icons::Icon; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; use gpui_native_utils::{focus_ring, record_anchor_bounds}; use std::rc::Rc; use crate::common::{Sizable, h_flex}; use crate::spinner::Spinner; /// The single activation handler, shared by pointer, Enter, and Space. type ClickHandler = Box; /// The tooltip view builder gpui's `.tooltip(...)` expects. type TooltipBuilder = Rc AnyView>; /// Which visual treatment a button uses. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ButtonVariant { /// The platform's default action. #[default] Primary, /// 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, loading: bool, tab_stop: bool, debug_selector: Option String>>, a11y_label: Option, tooltip: 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, loading: false, tab_stop: true, debug_selector: None, a11y_label: None, tooltip: 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, icon: I) -> Self where I: Into, { self.icon = Some(icon.into()); self } /// Shows a tooltip view next to the button after it is hovered. pub fn tooltip(mut self, tooltip: F) -> Self where F: Fn(&mut Window, &mut App) -> AnyView + 'static, { self.tooltip = Some(Rc::new(tooltip)); self } /// Sets the button's visual language. pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; self } /// Whether the button is a keyboard tab stop, dense toolbars turn it off. pub fn tab_stop(mut self, tab_stop: bool) -> Self { self.tab_stop = tab_stop; self } /// Sets the debug-bounds selector, for headless tests. pub fn debug_selector(mut self, selector: F) -> Self where F: Fn() -> String + 'static, { self.debug_selector = Some(Rc::new(selector)); self } /// Renders the button inert, unfocusable, and dimmed. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Shows the spinner in place of the icon and suppresses activation. pub fn loading(mut self, loading: bool) -> Self { self.loading = loading; 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 borders = theme.borders; let metrics = theme.sizes.resolve(self.size); let show_border = borders.buttons && self.variant != ButtonVariant::Ghost; 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_hover, ramps.secondary.pressed, ), ButtonVariant::Destructive => ( Some(ramps.destructive.base), colors.destructive_foreground, ramps.destructive.hover, ramps.destructive.pressed, ), }; let (border, hover_border, pressed_border) = if show_border { (colors.border, colors.border, colors.border) } else { match background { Some(fill) => (fill, hover, pressed), None => ( transparent_black(), transparent_black(), transparent_black(), ), } }; h_flex() .id(self.id.clone()) .when_some(self.debug_selector, |button, selector| { button.debug_selector(move || selector()) }) .role(Role::Button) .justify_center() .h(metrics.control_height + BORDER_WIDTH * 2.0) .when(self.icon.is_some(), |button| { button.px(metrics.dense_padding_x) }) .when(self.icon.is_none(), |button| button.px(metrics.padding_x)) .line_height(relative(1.0)) .gap_2() .rounded(theme.radius.control) .text_size(metrics.text_size) .border_1() .border_color(border) .when_some(background, |button, background| button.bg(background)) .text_color(foreground) .when_some(a11y_label, |button, label| button.aria_label(label)) .when(self.loading, |button| { button.child( Spinner::new((self.id.clone(), "spinner")) .with_size(self.size) .variant(crate::spinner::SpinnerVariant::Icon) .color(foreground), ) }) .when(!self.loading, |button| { button.when_some(self.icon, |button, icon| { button.child(icon.size(metrics.icon_size).color(foreground)) }) }) .when_some(self.label, |button, label| button.child(label)) .child( canvas(record_anchor_bounds(self.id.clone()), |_, _, _, _| {}) .absolute() .inset_0(), ) .when(self.disabled, |button| button.opacity(0.5)) .when_some(self.tooltip, |button, tooltip| { button.tooltip(move |window, cx| tooltip(window, cx)) }) .when(!self.disabled && !self.loading, |button| { button .cursor_pointer() .focusable() .tab_stop(self.tab_stop) .hover(move |style| style.bg(hover).border_color(hover_border)) .active(move |style| style.bg(pressed).border_color(pressed_border)) .focus_visible(|style| style.shadow(focus_ring(colors.ring))) .when_some(on_click, |button, on_click| { button.on_click(move |event, window, cx| on_click(event, window, cx)) }) }) } }