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}