1use std::sync::Arc;
2
3use gpui::{
4 CallbackHandle, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext,
5};
6
7use crate::prelude::*;
8use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
9
10/// Provides the flexibility to use either a standard
11/// button or an icon button in a given context.
12pub enum ButtonOrIconButton {
13 Button(Button),
14 IconButton(IconButton),
15}
16
17impl From<Button> for ButtonOrIconButton {
18 fn from(value: Button) -> Self {
19 Self::Button(value)
20 }
21}
22
23impl From<IconButton> for ButtonOrIconButton {
24 fn from(value: IconButton) -> Self {
25 Self::IconButton(value)
26 }
27}
28
29#[derive(Default, PartialEq, Clone, Copy)]
30pub enum IconPosition {
31 #[default]
32 Left,
33 Right,
34}
35
36#[derive(Default, Copy, Clone, PartialEq)]
37pub enum ButtonVariant {
38 #[default]
39 Ghost,
40 Filled,
41}
42
43impl ButtonVariant {
44 pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
45 match self {
46 ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
47 ButtonVariant::Filled => cx.theme().colors().element_background,
48 }
49 }
50
51 pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
52 match self {
53 ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
54 ButtonVariant::Filled => cx.theme().colors().element_hover,
55 }
56 }
57
58 pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
59 match self {
60 ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
61 ButtonVariant::Filled => cx.theme().colors().element_active,
62 }
63 }
64}
65
66// #[derive(Component)] <- todo
67pub struct Button {
68 disabled: bool,
69 click_handler: Option<CallbackHandle<()>>,
70 icon: Option<Icon>,
71 icon_position: Option<IconPosition>,
72 label: SharedString,
73 variant: ButtonVariant,
74 width: Option<DefiniteLength>,
75 color: Option<TextColor>,
76}
77
78impl Button {
79 pub fn new(label: impl Into<SharedString>) -> Self {
80 Self {
81 disabled: false,
82 click_handler: None,
83 icon: None,
84 icon_position: None,
85 label: label.into(),
86 variant: Default::default(),
87 width: Default::default(),
88 color: None,
89 }
90 }
91
92 pub fn ghost(label: impl Into<SharedString>) -> Self {
93 Self::new(label).variant(ButtonVariant::Ghost)
94 }
95
96 pub fn variant(mut self, variant: ButtonVariant) -> Self {
97 self.variant = variant;
98 self
99 }
100
101 pub fn icon(mut self, icon: Icon) -> Self {
102 self.icon = Some(icon);
103 self
104 }
105
106 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
107 if self.icon.is_none() {
108 panic!("An icon must be present if an icon_position is provided.");
109 }
110 self.icon_position = Some(icon_position);
111 self
112 }
113
114 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
115 self.width = width;
116 self
117 }
118
119 pub fn on_click(mut self, handler: CallbackHandle<()>) -> Self {
120 self.handlers.click = Some(handler);
121 self
122 }
123
124 pub fn disabled(mut self, disabled: bool) -> Self {
125 self.disabled = disabled;
126 self
127 }
128
129 pub fn color(mut self, color: Option<TextColor>) -> Self {
130 self.color = color;
131 self
132 }
133
134 pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
135 if self.disabled {
136 TextColor::Disabled
137 } else if let Some(color) = color {
138 color
139 } else {
140 Default::default()
141 }
142 }
143
144 fn render_label(&self, color: TextColor) -> Label {
145 Label::new(self.label.clone())
146 .color(color)
147 .line_height_style(LineHeightStyle::UILabel)
148 }
149
150 fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
151 self.icon.map(|i| IconElement::new(i).color(icon_color))
152 }
153
154 pub fn render(self, cx: &mut WindowContext) -> impl Component {
155 let (icon_color, label_color) = match (self.disabled, self.color) {
156 (true, _) => (TextColor::Disabled, TextColor::Disabled),
157 (_, None) => (TextColor::Default, TextColor::Default),
158 (_, Some(color)) => (TextColor::from(color), color),
159 };
160
161 let mut button = h_stack()
162 .id(SharedString::from(format!("{}", self.label)))
163 .relative()
164 .p_1()
165 .text_ui()
166 .rounded_md()
167 .bg(self.variant.bg_color(cx))
168 .cursor_pointer()
169 .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
170 .active(|style| style.bg(self.variant.bg_color_active(cx)));
171
172 match (self.icon, self.icon_position) {
173 (Some(_), Some(IconPosition::Left)) => {
174 button = button
175 .gap_1()
176 .child(self.render_label(label_color))
177 .children(self.render_icon(icon_color))
178 }
179 (Some(_), Some(IconPosition::Right)) => {
180 button = button
181 .gap_1()
182 .children(self.render_icon(icon_color))
183 .child(self.render_label(label_color))
184 }
185 (_, _) => button = button.child(self.render_label(label_color)),
186 }
187
188 if let Some(width) = self.width {
189 button = button.w(width).justify_center();
190 }
191
192 if let Some(click_handler) = self.handlers.click.clone() {
193 button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
194 click_handler(state, cx);
195 });
196 }
197
198 button
199 }
200}
201
202pub struct ButtonGroup {
203 buttons: Vec<Button>,
204}
205
206impl ButtonGroup {
207 pub fn new(buttons: Vec<Button>) -> Self {
208 Self { buttons }
209 }
210
211 fn render(self, cx: &mut WindowContext) -> impl Component {
212 let mut el = h_stack().text_ui();
213
214 for button in self.buttons {
215 el = el.child(button.render(cx));
216 }
217
218 el
219 }
220}
221
222#[cfg(feature = "stories")]
223pub use stories::*;
224
225#[cfg(feature = "stories")]
226mod stories {
227 use super::*;
228 use crate::{h_stack, v_stack, Story, TextColor};
229 use gpui::{rems, Div, Render};
230 use strum::IntoEnumIterator;
231
232 pub struct ButtonStory;
233
234 impl Render for ButtonStory {
235 type Element = Div;
236
237 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
238 let states = InteractionState::iter();
239
240 Story::container(cx)
241 .child(Story::title_for::<_, Button>(cx))
242 .child(
243 div()
244 .flex()
245 .gap_8()
246 .child(
247 div()
248 .child(Story::label(cx, "Ghost (Default)"))
249 .child(h_stack().gap_2().children(states.clone().map(|state| {
250 v_stack()
251 .gap_1()
252 .child(
253 Label::new(state.to_string()).color(TextColor::Muted),
254 )
255 .child(
256 Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
257 )
258 })))
259 .child(Story::label(cx, "Ghost – Left Icon"))
260 .child(h_stack().gap_2().children(states.clone().map(|state| {
261 v_stack()
262 .gap_1()
263 .child(
264 Label::new(state.to_string()).color(TextColor::Muted),
265 )
266 .child(
267 Button::new("Label")
268 .variant(ButtonVariant::Ghost)
269 .icon(Icon::Plus)
270 .icon_position(IconPosition::Left), // .state(state),
271 )
272 })))
273 .child(Story::label(cx, "Ghost – Right Icon"))
274 .child(h_stack().gap_2().children(states.clone().map(|state| {
275 v_stack()
276 .gap_1()
277 .child(
278 Label::new(state.to_string()).color(TextColor::Muted),
279 )
280 .child(
281 Button::new("Label")
282 .variant(ButtonVariant::Ghost)
283 .icon(Icon::Plus)
284 .icon_position(IconPosition::Right), // .state(state),
285 )
286 }))),
287 )
288 .child(
289 div()
290 .child(Story::label(cx, "Filled"))
291 .child(h_stack().gap_2().children(states.clone().map(|state| {
292 v_stack()
293 .gap_1()
294 .child(
295 Label::new(state.to_string()).color(TextColor::Muted),
296 )
297 .child(
298 Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
299 )
300 })))
301 .child(Story::label(cx, "Filled – Left Button"))
302 .child(h_stack().gap_2().children(states.clone().map(|state| {
303 v_stack()
304 .gap_1()
305 .child(
306 Label::new(state.to_string()).color(TextColor::Muted),
307 )
308 .child(
309 Button::new("Label")
310 .variant(ButtonVariant::Filled)
311 .icon(Icon::Plus)
312 .icon_position(IconPosition::Left), // .state(state),
313 )
314 })))
315 .child(Story::label(cx, "Filled – Right Button"))
316 .child(h_stack().gap_2().children(states.clone().map(|state| {
317 v_stack()
318 .gap_1()
319 .child(
320 Label::new(state.to_string()).color(TextColor::Muted),
321 )
322 .child(
323 Button::new("Label")
324 .variant(ButtonVariant::Filled)
325 .icon(Icon::Plus)
326 .icon_position(IconPosition::Right), // .state(state),
327 )
328 }))),
329 )
330 .child(
331 div()
332 .child(Story::label(cx, "Fixed With"))
333 .child(h_stack().gap_2().children(states.clone().map(|state| {
334 v_stack()
335 .gap_1()
336 .child(
337 Label::new(state.to_string()).color(TextColor::Muted),
338 )
339 .child(
340 Button::new("Label")
341 .variant(ButtonVariant::Filled)
342 // .state(state)
343 .width(Some(rems(6.).into())),
344 )
345 })))
346 .child(Story::label(cx, "Fixed With – Left Icon"))
347 .child(h_stack().gap_2().children(states.clone().map(|state| {
348 v_stack()
349 .gap_1()
350 .child(
351 Label::new(state.to_string()).color(TextColor::Muted),
352 )
353 .child(
354 Button::new("Label")
355 .variant(ButtonVariant::Filled)
356 // .state(state)
357 .icon(Icon::Plus)
358 .icon_position(IconPosition::Left)
359 .width(Some(rems(6.).into())),
360 )
361 })))
362 .child(Story::label(cx, "Fixed With – Right Icon"))
363 .child(h_stack().gap_2().children(states.clone().map(|state| {
364 v_stack()
365 .gap_1()
366 .child(
367 Label::new(state.to_string()).color(TextColor::Muted),
368 )
369 .child(
370 Button::new("Label")
371 .variant(ButtonVariant::Filled)
372 // .state(state)
373 .icon(Icon::Plus)
374 .icon_position(IconPosition::Right)
375 .width(Some(rems(6.).into())),
376 )
377 }))),
378 ),
379 )
380 .child(Story::label(cx, "Button with `on_click`"))
381 .child(
382 Button::new("Label")
383 .variant(ButtonVariant::Ghost)
384 .on_click(cx.callback(|_view, _, cx| println!("Button clicked."))),
385 )
386 }
387 }
388}