Skip to main content

ui/widgets/
buttons.rs

1//! The button — one component, three shipped looks. A catalog trait like
2//! every widget group: `use ui::widgets::{ButtonStyle, Buttons};` →
3//! `theme.button("Save", ButtonStyle::Prominent, None)`.
4//!
5//! [`ButtonStyle`] is a closed enum, not free-form knobs: it selects between
6//! the looks that ship, while per-call overrides stay chain modifiers.
7
8use gpui::{Div, SharedString, div, prelude::*, px};
9use motion::{self, Fade};
10use theme::{Theme, ThemeExt, ink, wash};
11
12/// The shipped looks (the reference `btnGhost` / `btnPrimary` /
13/// `btnDestructive`).
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ButtonStyle {
16    /// Quiet text on a translucent wash.
17    Ghost,
18    /// The maximum-contrast plate — the primary action.
19    Prominent,
20    /// The muted red fill — carries the destructive semantics with the paint.
21    Destructive,
22}
23
24/// The frame every style shares.
25fn frame(mut el: Div) -> Div {
26    el = el
27        .px(px(12.0))
28        .py(px(6.0))
29        .rounded(px(Theme::button_radius()))
30        .text_size(px(13.0))
31        .cursor_pointer();
32    el
33}
34
35pub trait Buttons: ThemeExt {
36    /// A labeled button in one of the shipped styles. `fade` matters only for
37    /// [`ButtonStyle::Ghost`]: `Some` animates the hover wash per instance,
38    /// `None` is the plain ghost with the hover left to the caller.
39    fn button(
40        &self,
41        label: impl Into<SharedString>,
42        style: ButtonStyle,
43        fade: Option<Fade>,
44    ) -> Div {
45        let theme = self.theme();
46        let label = label.into();
47        match style {
48            ButtonStyle::Ghost => match fade {
49                Some(fade) => {
50                    let mut btn = frame(div())
51                        .text_color(motion::hover_blend(&fade, theme.text_muted, theme.text))
52                        .bg(motion::hover_blend(&fade, wash(0.0), ink(0.06)))
53                        .child(label);
54                    btn.interactivity().on_hover(motion::hover_listener(fade));
55                    btn
56                }
57                None => frame(div()).text_color(theme.text_muted).child(label),
58            },
59            ButtonStyle::Prominent => frame(div())
60                .bg(theme.text)
61                .font_weight(gpui::FontWeight::MEDIUM)
62                .text_color(theme.on_solid)
63                .hover(|s| s.opacity(0.9))
64                .child(label),
65            ButtonStyle::Destructive => frame(div())
66                .bg(theme.danger_strong)
67                .font_weight(gpui::FontWeight::MEDIUM)
68                .text_color(gpui::white())
69                .hover(|s| s.opacity(0.9))
70                .child(label),
71        }
72    }
73}
74
75impl Buttons for Theme {}