Skip to main content

ccf_gpui_widgets/widgets/
button.rs

1//! Button utility functions for creating styled buttons
2//!
3//! These functions create pre-styled button elements using the theme system.
4//! They return focusable `Stateful<Div>` elements that can be composed with `.on_click()` handlers.
5//! Buttons support keyboard activation with Enter or Space when focused.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use ccf_gpui_widgets::widgets::{primary_button, secondary_button};
11//!
12//! let run_button = primary_button("run_btn", "Run", enabled, cx)
13//!     .on_click(cx.listener(|this, _, _, cx| {
14//!         this.run_action(cx);
15//!     }));
16//!
17//! let cancel_button = secondary_button("cancel_btn", "Cancel", cx)
18//!     .on_click(cx.listener(|this, _, _, cx| {
19//!         this.cancel_action(cx);
20//!     }));
21//! ```
22
23use gpui::prelude::*;
24use gpui::*;
25use crate::theme::get_theme;
26use crate::utils::darken;
27use super::focus_navigation::{FocusNext, FocusPrev};
28
29// Actions for button activation
30actions!(ccf_button, [ActivateButton]);
31
32/// Register key bindings for button components
33///
34/// Call this once at application startup:
35/// ```ignore
36/// ccf_gpui_widgets::widgets::button::register_keybindings(cx);
37/// ```
38pub fn register_keybindings(cx: &mut App) {
39    cx.bind_keys([
40        KeyBinding::new("enter", ActivateButton, Some("CcfButton")),
41        KeyBinding::new("space", ActivateButton, Some("CcfButton")),
42    ]);
43}
44
45/// Create a primary button with the specified label
46///
47/// The button is styled using the theme's primary colors when enabled,
48/// and disabled colors when not enabled. Buttons are focusable tab stops
49/// that can be activated with Enter or Space.
50///
51/// # Arguments
52///
53/// * `id` - Element ID for the button (required for click handlers)
54/// * `label` - The text to display on the button
55/// * `enabled` - Whether the button is enabled (affects styling and cursor)
56/// * `cx` - Application context to access the theme
57///
58/// # Returns
59///
60/// A focusable `Stateful<Div>` that can be composed with `.on_click()` and other handlers.
61pub fn primary_button(
62    id: impl Into<ElementId>,
63    label: &str,
64    enabled: bool,
65    cx: &App,
66) -> Stateful<Div> {
67    let theme = get_theme(cx);
68
69    div()
70        .id(id)
71        .key_context("CcfButton")
72        .focusable()
73        .tab_stop(enabled) // Disabled buttons are not tab stops
74        // Focus navigation (Tab / Shift+Tab)
75        .on_action(|_: &FocusNext, window, _cx| {
76            window.focus_next();
77        })
78        .on_action(|_: &FocusPrev, window, _cx| {
79            window.focus_prev();
80        })
81        .flex()
82        .items_center()
83        .justify_center()
84        .h(px(36.))
85        .px_4()
86        .rounded_md()
87        .cursor_pointer()
88        .text_sm()
89        .font_weight(FontWeight::MEDIUM)
90        .border_2()
91        .border_color(rgba(0x00000000)) // Invisible border by default
92        .when(enabled, |d| {
93            d.bg(rgb(theme.primary))
94                .text_color(rgb(theme.text_primary))
95                .hover(|d| d.bg(rgb(theme.primary_hover)))
96                .active(|d| d.bg(rgb(theme.primary_active)))
97        })
98        .when(!enabled, |d| {
99            d.bg(rgb(theme.disabled_bg))
100                .text_color(rgb(theme.disabled_text))
101                .cursor_default()
102        })
103        // Use contrasting color for focus on primary button (theme-aware)
104        .focus(|d| d.border_color(rgb(theme.border_focus_on_color)))
105        .child(label.to_string())
106}
107
108/// Create a secondary button with the specified label
109///
110/// Secondary buttons have a more subtle appearance with a border,
111/// suitable for less prominent actions. Buttons are focusable tab stops
112/// that can be activated with Enter or Space.
113///
114/// # Arguments
115///
116/// * `id` - Element ID for the button (required for click handlers)
117/// * `label` - The text to display on the button
118/// * `cx` - Application context to access the theme
119///
120/// # Returns
121///
122/// A focusable `Stateful<Div>` that can be composed with `.on_click()` and other handlers.
123pub fn secondary_button(
124    id: impl Into<ElementId>,
125    label: &str,
126    cx: &App,
127) -> Stateful<Div> {
128    let theme = get_theme(cx);
129
130    div()
131        .id(id)
132        .key_context("CcfButton")
133        .focusable()
134        .tab_stop(true)
135        // Focus navigation (Tab / Shift+Tab)
136        .on_action(|_: &FocusNext, window, _cx| {
137            window.focus_next();
138        })
139        .on_action(|_: &FocusPrev, window, _cx| {
140            window.focus_prev();
141        })
142        .flex()
143        .items_center()
144        .justify_center()
145        .h(px(36.))
146        .px_4()
147        .rounded_md()
148        .cursor_pointer()
149        .text_sm()
150        .font_weight(FontWeight::MEDIUM)
151        .bg(rgb(theme.secondary_bg))
152        .text_color(rgb(theme.text_primary))
153        .border_2()
154        .border_color(rgb(theme.secondary_border))
155        .hover(|d| d.bg(rgb(theme.secondary_bg_hover)))
156        .active(|d| d.bg(rgb(theme.secondary_bg_active)))
157        .focus(|d| d.border_color(rgb(theme.border_focus)))
158        .child(label.to_string())
159}
160
161/// Create a danger button with the specified label
162///
163/// Danger buttons are styled with error/red colors to indicate destructive actions
164/// like delete, remove, or irreversible operations. Buttons are focusable tab stops
165/// that can be activated with Enter or Space.
166///
167/// # Arguments
168///
169/// * `id` - Element ID for the button (required for click handlers)
170/// * `label` - The text to display on the button
171/// * `enabled` - Whether the button is enabled (affects styling and cursor)
172/// * `cx` - Application context to access the theme
173///
174/// # Returns
175///
176/// A focusable `Stateful<Div>` that can be composed with `.on_click()` and other handlers.
177///
178/// # Example
179///
180/// ```ignore
181/// use ccf_gpui_widgets::widgets::danger_button;
182///
183/// let delete_button = danger_button("delete_btn", "Delete", true, cx)
184///     .on_click(cx.listener(|this, _, _, cx| {
185///         this.delete_item(cx);
186///     }));
187/// ```
188pub fn danger_button(
189    id: impl Into<ElementId>,
190    label: &str,
191    enabled: bool,
192    cx: &App,
193) -> Stateful<Div> {
194    let theme = get_theme(cx);
195
196    // Darker variants of error color for hover/active states
197    let danger_hover = darken(theme.error, 0.15);
198    let danger_active = darken(theme.error, 0.25);
199
200    div()
201        .id(id)
202        .key_context("CcfButton")
203        .focusable()
204        .tab_stop(enabled) // Disabled buttons are not tab stops
205        // Focus navigation (Tab / Shift+Tab)
206        .on_action(|_: &FocusNext, window, _cx| {
207            window.focus_next();
208        })
209        .on_action(|_: &FocusPrev, window, _cx| {
210            window.focus_prev();
211        })
212        .flex()
213        .items_center()
214        .justify_center()
215        .h(px(36.))
216        .px_4()
217        .rounded_md()
218        .cursor_pointer()
219        .text_sm()
220        .font_weight(FontWeight::MEDIUM)
221        .border_2()
222        .border_color(rgba(0x00000000)) // Invisible border by default
223        .when(enabled, |d| {
224            d.bg(rgb(theme.error))
225                .text_color(rgb(theme.text_primary))
226                .hover(|d| d.bg(rgb(danger_hover)))
227                .active(|d| d.bg(rgb(danger_active)))
228        })
229        .when(!enabled, |d| {
230            d.bg(rgb(theme.disabled_bg))
231                .text_color(rgb(theme.disabled_text))
232                .cursor_default()
233        })
234        // Use contrasting color for focus on danger button
235        .focus(|d| d.border_color(rgb(theme.border_focus_on_color)))
236        .child(label.to_string())
237}