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