Skip to main content

ui/widgets/
button.rs

1//! A button with stable focus and one activation callback.
2
3use std::rc::Rc;
4
5use gpui::{
6    App, ElementId, IntoElement, Refineable, RenderOnce, SharedString, StyleRefinement, Window,
7    div, prelude::*, px,
8};
9use icons::Icon;
10use motion::Fade;
11use theme::{ControlSize, Sizing, Theme};
12
13use super::{ButtonRole, ButtonStyle, buttons::appearance};
14
15/// A semantic button. Initialize [`crate::focus`] and mount its traversal
16/// handler on the window root; identity retains focus across renders.
17#[derive(IntoElement)]
18pub struct Button {
19    id: ElementId,
20    label: SharedString,
21    icon: Option<Icon>,
22    label_hidden: bool,
23    style: ButtonStyle,
24    size: ControlSize,
25    role: Option<ButtonRole>,
26    fade: Option<Fade>,
27    enabled: bool,
28    refinement: StyleRefinement,
29    activate: Option<Rc<Activation>>,
30}
31
32type Activation = dyn Fn(&(), &mut Window, &mut App);
33
34impl Button {
35    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
36        Self {
37            id: id.into(),
38            label: label.into(),
39            icon: None,
40            label_hidden: false,
41            style: ButtonStyle::Ghost,
42            size: ControlSize::Regular,
43            role: None,
44            fade: None,
45            enabled: true,
46            refinement: StyleRefinement::default(),
47            activate: None,
48        }
49    }
50
51    pub fn button_style(mut self, style: ButtonStyle) -> Self {
52        self.style = style;
53        self
54    }
55
56    /// Resolve label and icon geometry together after configuration.
57    pub fn control_size(mut self, size: ControlSize) -> Self {
58        self.size = size;
59        self
60    }
61
62    pub fn role(mut self, role: ButtonRole) -> Self {
63        self.role = Some(role);
64        self
65    }
66
67    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
68        self.icon = Some(icon.into());
69        self
70    }
71
72    /// Hide the visual label while retaining its accessible name.
73    pub fn label_hidden(mut self) -> Self {
74        self.label_hidden = true;
75        self
76    }
77
78    pub fn hover_fade(mut self, fade: Fade) -> Self {
79        self.fade = Some(fade);
80        self
81    }
82
83    pub fn enabled(mut self, enabled: bool) -> Self {
84        self.enabled = enabled;
85        self
86    }
87
88    /// Accepts `cx.listener(...)`; both Enter/Space and clicks call it once.
89    pub fn on_press(mut self, activate: impl Fn(&(), &mut Window, &mut App) + 'static) -> Self {
90        self.activate = Some(Rc::new(activate));
91        self
92    }
93}
94
95impl Styled for Button {
96    fn style(&mut self) -> &mut StyleRefinement {
97        &mut self.refinement
98    }
99}
100
101impl RenderOnce for Button {
102    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
103        let focus = window.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle());
104        let focus = focus.read(cx).clone();
105        let theme = Theme::of(cx);
106        let enabled = self.enabled && self.activate.is_some();
107        let frame = div()
108            .control_size(self.size)
109            // The permanent focus border occupies one point on each edge.
110            .py(px((self.size.pad_y() - 1.0).max(0.0)))
111            .flex()
112            .items_center()
113            .justify_center()
114            .gap(px(Theme::SPACE))
115            .border_1()
116            .border_color(super::RING_SLOT)
117            .when(self.label_hidden, |el| {
118                el.px(px(0.0)).w(px(self.size.height()))
119            });
120        let fade = if enabled { self.fade } else { None };
121        let plain_ghost = self.style == ButtonStyle::Ghost && fade.is_none();
122        let (mut frame, tint) = appearance(theme, frame, self.style, self.role, fade, enabled);
123        if enabled && plain_ghost {
124            frame = frame.hover(|style| style.bg(theme.element_hover));
125        }
126        let tint = self.refinement.text.color.unwrap_or(tint);
127        frame.style().refine(&self.refinement);
128        let frame = frame
129            .when_some(self.icon, |el, icon| {
130                el.child(crate::icons::icon(icon).size(px(14.0)).text_color(tint))
131            })
132            .when(!self.label_hidden, |el| el.child(self.label.clone()))
133            .when(enabled, |el| el.cursor_pointer())
134            .when(!enabled, |el| el.opacity(0.5))
135            .id(self.id)
136            .role(gpui::Role::Button)
137            .aria_label(self.label);
138        crate::focus::pressable(theme, &focus, frame, enabled, move |event, window, cx| {
139            if let Some(activate) = &self.activate {
140                activate(event, window, cx);
141            }
142        })
143    }
144}