1use std::marker::PhantomData;
2use std::rc::Rc;
3
4use gpui3::{DefiniteLength, Hsla, MouseButton, WindowContext};
5
6use crate::prelude::*;
7use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
8
9#[derive(Default, PartialEq, Clone, Copy)]
10pub enum IconPosition {
11 #[default]
12 Left,
13 Right,
14}
15
16#[derive(Default, Copy, Clone, PartialEq)]
17pub enum ButtonVariant {
18 #[default]
19 Ghost,
20 Filled,
21}
22
23// struct ButtonHandlers<V> {
24// click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
25// }
26
27// impl<V> Default for ButtonHandlers<V> {
28// fn default() -> Self {
29// Self { click: None }
30// }
31// }
32
33#[derive(Element)]
34pub struct Button<S: 'static + Send + Sync + Clone> {
35 state_type: PhantomData<S>,
36 label: String,
37 variant: ButtonVariant,
38 state: InteractionState,
39 icon: Option<Icon>,
40 icon_position: Option<IconPosition>,
41 width: Option<DefiniteLength>,
42 // handlers: ButtonHandlers<S>,
43}
44
45impl<S: 'static + Send + Sync + Clone> Button<S> {
46 pub fn new<L>(label: L) -> Self
47 where
48 L: Into<String>,
49 {
50 Self {
51 state_type: PhantomData,
52 label: label.into(),
53 variant: Default::default(),
54 state: Default::default(),
55 icon: None,
56 icon_position: None,
57 width: Default::default(),
58 // handlers: ButtonHandlers::default(),
59 }
60 }
61
62 pub fn ghost<L>(label: L) -> Self
63 where
64 L: Into<String>,
65 {
66 Self::new(label).variant(ButtonVariant::Ghost)
67 }
68
69 pub fn variant(mut self, variant: ButtonVariant) -> Self {
70 self.variant = variant;
71 self
72 }
73
74 pub fn state(mut self, state: InteractionState) -> Self {
75 self.state = state;
76 self
77 }
78
79 pub fn icon(mut self, icon: Icon) -> Self {
80 self.icon = Some(icon);
81 self
82 }
83
84 pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
85 if self.icon.is_none() {
86 panic!("An icon must be present if an icon_position is provided.");
87 }
88 self.icon_position = Some(icon_position);
89 self
90 }
91
92 pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
93 self.width = width;
94 self
95 }
96
97 // pub fn on_click(mut self, handler: impl Fn(&mut S, &mut EventContext<S>) + 'static) -> Self {
98 // self.handlers.click = Some(Rc::new(handler));
99 // self
100 // }
101
102 fn background_color(&self, cx: &mut ViewContext<S>) -> Hsla {
103 let theme = theme(cx);
104 let system_color = SystemColor::new();
105
106 match (self.variant, self.state) {
107 (ButtonVariant::Ghost, InteractionState::Hovered) => {
108 theme.lowest.base.hovered.background
109 }
110 (ButtonVariant::Ghost, InteractionState::Active) => {
111 theme.lowest.base.pressed.background
112 }
113 (ButtonVariant::Filled, InteractionState::Enabled) => {
114 theme.lowest.on.default.background
115 }
116 (ButtonVariant::Filled, InteractionState::Hovered) => {
117 theme.lowest.on.hovered.background
118 }
119 (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
120 (ButtonVariant::Filled, InteractionState::Disabled) => {
121 theme.lowest.on.disabled.background
122 }
123 _ => system_color.transparent,
124 }
125 }
126
127 fn label_color(&self) -> LabelColor {
128 match self.state {
129 InteractionState::Disabled => LabelColor::Disabled,
130 _ => Default::default(),
131 }
132 }
133
134 fn icon_color(&self) -> IconColor {
135 match self.state {
136 InteractionState::Disabled => IconColor::Disabled,
137 _ => Default::default(),
138 }
139 }
140
141 fn border_color(&self, cx: &WindowContext) -> Hsla {
142 let theme = theme(cx);
143 let system_color = SystemColor::new();
144
145 match self.state {
146 InteractionState::Focused => theme.lowest.accent.default.border,
147 _ => system_color.transparent,
148 }
149 }
150
151 fn render_label(&self) -> Label<S> {
152 Label::new(self.label.clone())
153 .size(LabelSize::Small)
154 .color(self.label_color())
155 }
156
157 fn render_icon(&self, icon_color: IconColor) -> Option<IconElement<S>> {
158 self.icon.map(|i| IconElement::new(i).color(icon_color))
159 }
160
161 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
162 let theme = theme(cx);
163 let icon_color = self.icon_color();
164 let system_color = SystemColor::new();
165 let border_color = self.border_color(cx);
166
167 let mut el = h_stack()
168 .h_6()
169 .px_1()
170 .items_center()
171 .rounded_md()
172 .border()
173 .border_color(border_color)
174 .fill(self.background_color(cx));
175
176 match (self.icon, self.icon_position) {
177 (Some(_), Some(IconPosition::Left)) => {
178 el = el
179 .gap_1()
180 .child(self.render_label())
181 .children(self.render_icon(icon_color))
182 }
183 (Some(_), Some(IconPosition::Right)) => {
184 el = el
185 .gap_1()
186 .children(self.render_icon(icon_color))
187 .child(self.render_label())
188 }
189 (_, _) => el = el.child(self.render_label()),
190 }
191
192 if let Some(width) = self.width {
193 el = el.w(width).justify_center();
194 }
195
196 // if let Some(click_handler) = self.handlers.click.clone() {
197 // el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
198 // click_handler(view, cx);
199 // });
200 // }
201
202 el
203 }
204}
205
206#[cfg(feature = "stories")]
207pub use stories::*;
208
209#[cfg(feature = "stories")]
210mod stories {
211 use gpui3::rems;
212 use strum::IntoEnumIterator;
213
214 use crate::{h_stack, v_stack, LabelColor, LabelSize, Story};
215
216 use super::*;
217
218 #[derive(Element)]
219 pub struct ButtonStory<S: 'static + Send + Sync + Clone> {
220 state_type: PhantomData<S>,
221 }
222
223 impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
224 pub fn new() -> Self {
225 Self {
226 state_type: PhantomData,
227 }
228 }
229
230 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
231 let states = InteractionState::iter();
232
233 Story::container(cx)
234 .child(Story::title_for::<_, Button<S>>(cx))
235 .child(
236 div()
237 .flex()
238 .gap_8()
239 .child(
240 div()
241 .child(Story::label(cx, "Ghost (Default)"))
242 .child(h_stack().gap_2().children(states.clone().map(|state| {
243 v_stack()
244 .gap_1()
245 .child(
246 Label::new(state.to_string())
247 .color(LabelColor::Muted)
248 .size(LabelSize::Small),
249 )
250 .child(
251 Button::new("Label")
252 .variant(ButtonVariant::Ghost)
253 .state(state),
254 )
255 })))
256 .child(Story::label(cx, "Ghost – Left Icon"))
257 .child(h_stack().gap_2().children(states.clone().map(|state| {
258 v_stack()
259 .gap_1()
260 .child(
261 Label::new(state.to_string())
262 .color(LabelColor::Muted)
263 .size(LabelSize::Small),
264 )
265 .child(
266 Button::new("Label")
267 .variant(ButtonVariant::Ghost)
268 .icon(Icon::Plus)
269 .icon_position(IconPosition::Left)
270 .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())
279 .color(LabelColor::Muted)
280 .size(LabelSize::Small),
281 )
282 .child(
283 Button::new("Label")
284 .variant(ButtonVariant::Ghost)
285 .icon(Icon::Plus)
286 .icon_position(IconPosition::Right)
287 .state(state),
288 )
289 }))),
290 )
291 .child(
292 div()
293 .child(Story::label(cx, "Filled"))
294 .child(h_stack().gap_2().children(states.clone().map(|state| {
295 v_stack()
296 .gap_1()
297 .child(
298 Label::new(state.to_string())
299 .color(LabelColor::Muted)
300 .size(LabelSize::Small),
301 )
302 .child(
303 Button::new("Label")
304 .variant(ButtonVariant::Filled)
305 .state(state),
306 )
307 })))
308 .child(Story::label(cx, "Filled – Left Button"))
309 .child(h_stack().gap_2().children(states.clone().map(|state| {
310 v_stack()
311 .gap_1()
312 .child(
313 Label::new(state.to_string())
314 .color(LabelColor::Muted)
315 .size(LabelSize::Small),
316 )
317 .child(
318 Button::new("Label")
319 .variant(ButtonVariant::Filled)
320 .icon(Icon::Plus)
321 .icon_position(IconPosition::Left)
322 .state(state),
323 )
324 })))
325 .child(Story::label(cx, "Filled – Right Button"))
326 .child(h_stack().gap_2().children(states.clone().map(|state| {
327 v_stack()
328 .gap_1()
329 .child(
330 Label::new(state.to_string())
331 .color(LabelColor::Muted)
332 .size(LabelSize::Small),
333 )
334 .child(
335 Button::new("Label")
336 .variant(ButtonVariant::Filled)
337 .icon(Icon::Plus)
338 .icon_position(IconPosition::Right)
339 .state(state),
340 )
341 }))),
342 )
343 .child(
344 div()
345 .child(Story::label(cx, "Fixed With"))
346 .child(h_stack().gap_2().children(states.clone().map(|state| {
347 v_stack()
348 .gap_1()
349 .child(
350 Label::new(state.to_string())
351 .color(LabelColor::Muted)
352 .size(LabelSize::Small),
353 )
354 .child(
355 Button::new("Label")
356 .variant(ButtonVariant::Filled)
357 .state(state)
358 .width(Some(rems(6.).into())),
359 )
360 })))
361 .child(Story::label(cx, "Fixed With – Left Icon"))
362 .child(h_stack().gap_2().children(states.clone().map(|state| {
363 v_stack()
364 .gap_1()
365 .child(
366 Label::new(state.to_string())
367 .color(LabelColor::Muted)
368 .size(LabelSize::Small),
369 )
370 .child(
371 Button::new("Label")
372 .variant(ButtonVariant::Filled)
373 .state(state)
374 .icon(Icon::Plus)
375 .icon_position(IconPosition::Left)
376 .width(Some(rems(6.).into())),
377 )
378 })))
379 .child(Story::label(cx, "Fixed With – Right Icon"))
380 .child(h_stack().gap_2().children(states.clone().map(|state| {
381 v_stack()
382 .gap_1()
383 .child(
384 Label::new(state.to_string())
385 .color(LabelColor::Muted)
386 .size(LabelSize::Small),
387 )
388 .child(
389 Button::new("Label")
390 .variant(ButtonVariant::Filled)
391 .state(state)
392 .icon(Icon::Plus)
393 .icon_position(IconPosition::Right)
394 .width(Some(rems(6.).into())),
395 )
396 }))),
397 ),
398 )
399 .child(Story::label(cx, "Button with `on_click`"))
400 .child(
401 Button::new("Label").variant(ButtonVariant::Ghost), // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire.
402 // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire.
403 // .on_click(|_view, _cx| println!("Button clicked.")),
404 )
405 }
406 }
407}