1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, Div, FocusHandle, Hsla, InteractiveElement, IntoElement, ParentElement,
5 RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
6 prelude::FluentBuilder, px,
7};
8use gpui_kit_assets::{Icon, icon};
9use gpui_kit_semantics::{NodeSpec, Role, Semantic};
10use gpui_kit_theme::{ActiveTheme, ControlMetrics, ControlSize, Radius, Theme, TypeScale};
11
12use crate::foundation::direction::{ActiveDirection, DirectionalExt, LayoutDirection};
13use crate::foundation::{
14 Disableable, FocusRing, Ident, Pressable, Selectable, Sizable, StyledExt,
15 text as foundation_text,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ButtonVariant {
22 #[default]
23 Primary,
24 Secondary,
25 Ghost,
26 Danger,
27 Link,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum IconPosition {
33 #[default]
34 Leading,
35 Trailing,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum ButtonJoin {
44 #[default]
45 Alone,
46 Leading,
47 Middle,
48 Trailing,
49}
50
51type ClickHandler = Rc<dyn Fn(&mut Window, &mut App)>;
52
53#[derive(IntoElement)]
58pub struct Button {
59 ident: Ident,
60 semantic_parent: Option<SharedString>,
61 focus_handle: Option<FocusHandle>,
62 label: Option<SharedString>,
63 name: Option<SharedString>,
66 description: Option<SharedString>,
67 glyph: Option<Icon>,
68 icon_position: IconPosition,
69 variant: ButtonVariant,
70 size: ControlSize,
71 disabled: bool,
72 selected: bool,
73 checked: Option<bool>,
74 loading: bool,
75 full_width: bool,
76 icon_only: bool,
77 join: ButtonJoin,
78 on_click: Option<ClickHandler>,
79}
80
81impl std::fmt::Debug for Button {
82 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 formatter
84 .debug_struct("Button")
85 .field("ident", &self.ident)
86 .field("label", &self.label)
87 .field("variant", &self.variant)
88 .field("size", &self.size)
89 .field("disabled", &self.disabled)
90 .field("selected", &self.selected)
91 .field("loading", &self.loading)
92 .field("has_handler", &self.on_click.is_some())
93 .finish()
94 }
95}
96
97impl Button {
98 pub fn new(ident: impl Into<Ident>) -> Self {
99 Self {
100 ident: ident.into(),
101 semantic_parent: None,
102 focus_handle: None,
103 label: None,
104 name: None,
105 description: None,
106 glyph: None,
107 icon_position: IconPosition::Leading,
108 variant: ButtonVariant::default(),
109 size: ControlSize::default(),
110 disabled: false,
111 selected: false,
112 checked: None,
113 loading: false,
114 full_width: false,
115 icon_only: false,
116 join: ButtonJoin::Alone,
117 on_click: None,
118 }
119 }
120
121 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
122 self.label = Some(label.into());
123 self
124 }
125
126 pub fn accessible_name(mut self, name: impl Into<SharedString>) -> Self {
130 self.name = Some(name.into());
131 self
132 }
133
134 pub fn accessible_description(mut self, description: impl Into<SharedString>) -> Self {
136 self.description = Some(description.into());
137 self
138 }
139
140 pub fn icon_only(mut self, glyph: Icon, name: impl Into<SharedString>) -> Self {
145 self.glyph = Some(glyph);
146 self.label = None;
147 self.icon_only = true;
148 self.name = Some(name.into());
149 self
150 }
151
152 pub fn join(mut self, join: ButtonJoin) -> Self {
154 self.join = join;
155 self
156 }
157
158 pub fn semantic_parent(mut self, parent: impl Into<SharedString>) -> Self {
161 self.semantic_parent = Some(parent.into());
162 self
163 }
164
165 pub fn icon(mut self, glyph: Icon) -> Self {
166 self.glyph = Some(glyph);
167 self
168 }
169
170 pub fn icon_position(mut self, position: IconPosition) -> Self {
171 self.icon_position = position;
172 self
173 }
174
175 pub fn variant(mut self, variant: ButtonVariant) -> Self {
176 self.variant = variant;
177 self
178 }
179
180 pub fn primary(self) -> Self {
181 self.variant(ButtonVariant::Primary)
182 }
183
184 pub fn secondary(self) -> Self {
185 self.variant(ButtonVariant::Secondary)
186 }
187
188 pub fn ghost(self) -> Self {
189 self.variant(ButtonVariant::Ghost)
190 }
191
192 pub fn danger(self) -> Self {
193 self.variant(ButtonVariant::Danger)
194 }
195
196 pub fn link(self) -> Self {
197 self.variant(ButtonVariant::Link)
198 }
199
200 pub fn loading(mut self, loading: bool) -> Self {
202 self.loading = loading;
203 self
204 }
205
206 pub fn full_width(mut self, full_width: bool) -> Self {
207 self.full_width = full_width;
208 self
209 }
210
211 pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
217 self.focus_handle = Some(handle.clone());
218 self
219 }
220
221 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
222 self.on_click = Some(Rc::new(handler));
223 self
224 }
225
226 pub fn checked_state(mut self, checked: bool) -> Self {
232 self.checked = Some(checked);
233 self
234 }
235
236 fn actionable(&self) -> bool {
237 !self.disabled && !self.loading && self.on_click.is_some()
238 }
239
240 fn announced_name(&self) -> Option<SharedString> {
241 self.name.clone().or_else(|| self.label.clone())
242 }
243}
244
245impl Disableable for Button {
246 fn disabled(mut self, disabled: bool) -> Self {
247 self.disabled = disabled;
248 self
249 }
250}
251
252impl Selectable for Button {
253 fn selected(mut self, selected: bool) -> Self {
254 self.selected = selected;
255 self
256 }
257}
258
259impl Sizable for Button {
260 fn control_size(mut self, size: ControlSize) -> Self {
261 self.size = size;
262 self
263 }
264}
265
266impl RenderOnce for Button {
267 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
268 let theme = cx.theme().clone();
269 let metrics = theme.control.get(self.size);
270 let inert = self.disabled || self.loading;
271 let direction = cx.layout_direction();
272 let actionable = self.actionable();
273 let hover_group = self.ident.child("hover").semantic_id();
274
275 let mut content: Vec<AnyElement> = Vec::new();
276 let glyph = self.glyph.map(|glyph| {
277 icon(glyph)
280 .size(px(metrics.icon_size))
281 .flex_none()
282 .text_color(foreground(&theme, self.variant))
283 .when(!inert && self.variant == ButtonVariant::Ghost, |element| {
284 element.group_hover(hover_group.clone(), |style| {
285 style.text_color(theme.colors.text)
286 })
287 })
288 .when(!inert && self.variant == ButtonVariant::Link, |element| {
289 element.group_hover(hover_group.clone(), |style| {
290 style.text_color(theme.colors.accent_strong)
291 })
292 })
293 .into_any_element()
294 });
295 if let Some(glyph) = glyph {
296 match self.icon_position {
297 IconPosition::Leading => content.push(glyph),
298 IconPosition::Trailing => content.insert(0, glyph),
299 }
300 }
301 if let Some(label) = self.label.clone() {
302 let label = foundation_text(&theme, TypeScale::Label, label)
303 .text_size(px(metrics.font_size))
304 .text_color(foreground(&theme, self.variant))
305 .when(!inert && self.variant == ButtonVariant::Ghost, |element| {
306 element.group_hover(hover_group.clone(), |style| {
307 style.text_color(theme.colors.text)
308 })
309 })
310 .when(!inert && self.variant == ButtonVariant::Link, |element| {
311 element.group_hover(hover_group.clone(), |style| {
312 style.text_color(theme.colors.accent_strong)
313 })
314 })
315 .flex_none()
316 .into_any_element();
317 match self.icon_position {
318 IconPosition::Leading => content.push(label),
319 IconPosition::Trailing => content.insert(0, label),
320 }
321 }
322
323 let mut button = frame(&theme, self.variant, metrics, inert, direction)
324 .group(hover_group)
325 .when(self.icon_only, |element| {
326 element.w(px(metrics.height)).px(px(0.0))
327 })
328 .map(|element| joined(element, &theme, self.join, direction))
329 .when(self.selected, |element| {
330 element
331 .bg(theme.colors.selected)
332 .border_color(theme.colors.hairline_strong)
333 })
334 .id(self.ident.element_id())
335 .when_some(self.focus_handle.clone(), |element, handle| {
336 element.track_focus(&handle)
337 })
338 .role(gpui::Role::Button)
339 .when(self.full_width, |element| element.w_full())
340 .when(actionable, |element| {
341 element
342 .cursor_pointer()
343 .tab_index(0)
344 .focus_ring(&theme)
345 .pressable(cx)
346 })
347 .children(content);
348
349 if let (true, Some(handler)) = (actionable, self.on_click.clone()) {
350 let on_click = Rc::clone(&handler);
351 button
352 .interactivity()
353 .on_click(move |_, window, cx| on_click(window, cx));
354 button
355 .interactivity()
356 .on_key_down(move |event, window, cx| {
357 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
358 handler(window, cx);
359 cx.stop_propagation();
360 }
361 });
362 }
363
364 let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::Button)
365 .disabled(inert)
366 .busy(self.loading);
367 if let Some(parent) = self.semantic_parent.clone() {
368 spec = spec.parent(parent);
369 }
370 match self.checked {
371 Some(checked) => spec = spec.checked(checked),
372 None if self.selected => spec = spec.checked(true),
373 None => {}
374 }
375 if let Some(handle) = &self.focus_handle {
376 spec = spec.focus(handle);
377 }
378 if let Some(name) = self.announced_name() {
379 spec = spec.text(name);
380 }
381 if let Some(description) = self.description {
382 spec = spec.description(description);
383 }
384 button.semantic_in(cx, spec)
385 }
386}
387
388fn joined(element: Div, theme: &Theme, join: ButtonJoin, direction: LayoutDirection) -> Div {
391 let flat = px(0.0);
392 let overlap = px(-theme.borders.hairline);
393 let start_flat = |element: Div| {
397 if direction.is_rtl() {
398 element.rounded_tr(flat).rounded_br(flat)
399 } else {
400 element.rounded_tl(flat).rounded_bl(flat)
401 }
402 };
403 let end_flat = |element: Div| {
404 if direction.is_rtl() {
405 element.rounded_tl(flat).rounded_bl(flat)
406 } else {
407 element.rounded_tr(flat).rounded_br(flat)
408 }
409 };
410 match join {
411 ButtonJoin::Alone => element,
412 ButtonJoin::Leading => end_flat(element),
413 ButtonJoin::Middle => end_flat(start_flat(element.ms(direction, overlap))),
414 ButtonJoin::Trailing => start_flat(element.ms(direction, overlap)),
415 }
416}
417
418fn foreground(theme: &Theme, variant: ButtonVariant) -> Hsla {
419 match variant {
420 ButtonVariant::Primary => theme.colors.text_on_accent,
421 ButtonVariant::Secondary => theme.colors.text,
422 ButtonVariant::Ghost => theme.colors.text_muted,
423 ButtonVariant::Danger => gpui::white(),
424 ButtonVariant::Link => theme.colors.accent,
425 }
426}
427
428fn frame(
429 theme: &Theme,
430 variant: ButtonVariant,
431 metrics: ControlMetrics,
432 inert: bool,
433 direction: LayoutDirection,
434) -> Div {
435 let base = div()
439 .row_reading(direction)
440 .justify_center()
441 .flex_none()
442 .h(px(metrics.height))
443 .gap(px(metrics.gap))
444 .px(px(metrics.padding_x))
445 .radius(theme, Radius::Control)
446 .border(px(theme.borders.hairline))
447 .border_color(gpui::transparent_black())
448 .when(inert, |element| element.opacity(theme.opacity.disabled));
449
450 match variant {
451 ButtonVariant::Primary => base
452 .bg(theme.colors.text)
453 .when(!inert, |element| element.hover(|style| style.opacity(0.9))),
454 ButtonVariant::Secondary => base
455 .bg(theme.colors.raised)
456 .border_color(theme.colors.hairline)
457 .when(!inert, |element| {
458 element.hover(|style| style.bg(theme.colors.hover))
459 }),
460 ButtonVariant::Ghost => base.when(!inert, |element| {
461 element.hover(|style| style.bg(theme.colors.hover))
462 }),
463 ButtonVariant::Danger => base
464 .bg(theme.colors.danger.opacity(0.8))
465 .when(!inert, |element| element.hover(|style| style.opacity(0.9))),
466 ButtonVariant::Link => base.px(px(0.0)),
467 }
468}
469
470#[derive(IntoElement)]
478pub struct IconButton {
479 button: Button,
480}
481
482impl std::fmt::Debug for IconButton {
483 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 formatter
485 .debug_struct("IconButton")
486 .field("button", &self.button)
487 .finish()
488 }
489}
490
491impl IconButton {
492 pub fn new(ident: impl Into<Ident>, glyph: Icon, name: impl Into<SharedString>) -> Self {
493 Self {
494 button: Button::new(ident)
495 .ghost()
496 .icon_only(glyph, name)
497 .icon_position(IconPosition::Leading),
498 }
499 }
500
501 pub fn variant(mut self, variant: ButtonVariant) -> Self {
502 self.button = self.button.variant(variant);
503 self
504 }
505
506 pub fn primary(self) -> Self {
507 self.variant(ButtonVariant::Primary)
508 }
509
510 pub fn secondary(self) -> Self {
511 self.variant(ButtonVariant::Secondary)
512 }
513
514 pub fn ghost(self) -> Self {
515 self.variant(ButtonVariant::Ghost)
516 }
517
518 pub fn danger(self) -> Self {
519 self.variant(ButtonVariant::Danger)
520 }
521
522 pub fn loading(mut self, loading: bool) -> Self {
523 self.button = self.button.loading(loading);
524 self
525 }
526
527 pub fn semantic_parent(mut self, parent: impl Into<SharedString>) -> Self {
528 self.button = self.button.semantic_parent(parent);
529 self
530 }
531
532 pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
533 self.button = self.button.track_focus(handle);
534 self
535 }
536
537 pub fn join(mut self, join: ButtonJoin) -> Self {
538 self.button = self.button.join(join);
539 self
540 }
541
542 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
543 self.button = self.button.on_click(handler);
544 self
545 }
546}
547
548impl Disableable for IconButton {
549 fn disabled(mut self, disabled: bool) -> Self {
550 self.button = self.button.disabled(disabled);
551 self
552 }
553}
554
555impl Selectable for IconButton {
556 fn selected(mut self, selected: bool) -> Self {
557 self.button = self.button.selected(selected);
558 self
559 }
560}
561
562impl Sizable for IconButton {
563 fn control_size(mut self, size: ControlSize) -> Self {
564 self.button = self.button.control_size(size);
565 self
566 }
567}
568
569impl RenderOnce for IconButton {
570 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
571 self.button
572 }
573}
574
575#[derive(IntoElement)]
582pub struct ButtonGroup {
583 ident: Ident,
584 buttons: Vec<Button>,
585 size: ControlSize,
586 disabled: bool,
587}
588
589impl std::fmt::Debug for ButtonGroup {
590 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591 formatter
592 .debug_struct("ButtonGroup")
593 .field("ident", &self.ident)
594 .field("buttons", &self.buttons.len())
595 .field("disabled", &self.disabled)
596 .finish()
597 }
598}
599
600impl ButtonGroup {
601 pub fn new(ident: impl Into<Ident>) -> Self {
602 Self {
603 ident: ident.into(),
604 buttons: Vec::new(),
605 size: ControlSize::default(),
606 disabled: false,
607 }
608 }
609
610 pub fn child(mut self, button: Button) -> Self {
611 self.buttons.push(button);
612 self
613 }
614
615 pub fn children(mut self, buttons: impl IntoIterator<Item = Button>) -> Self {
616 self.buttons.extend(buttons);
617 self
618 }
619}
620
621impl Disableable for ButtonGroup {
622 fn disabled(mut self, disabled: bool) -> Self {
623 self.disabled = disabled;
624 self
625 }
626}
627
628impl Sizable for ButtonGroup {
629 fn control_size(mut self, size: ControlSize) -> Self {
630 self.size = size;
631 self
632 }
633}
634
635impl RenderOnce for ButtonGroup {
636 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
637 let last = self.buttons.len().saturating_sub(1);
638 let parent = self.ident.semantic_id();
639 let group_disabled = self.disabled;
640 let size = self.size;
641 let buttons = self
642 .buttons
643 .into_iter()
644 .enumerate()
645 .map(|(index, button)| {
646 let join = match (index, last) {
647 (_, 0) => ButtonJoin::Alone,
648 (0, _) => ButtonJoin::Leading,
649 (index, last) if index == last => ButtonJoin::Trailing,
650 _ => ButtonJoin::Middle,
651 };
652 let button = button
655 .join(join)
656 .control_size(size)
657 .semantic_parent(parent.clone());
658 if group_disabled {
659 button.disabled(true)
660 } else {
661 button
662 }
663 })
664 .collect::<Vec<_>>();
665
666 div()
667 .row_reading(cx.layout_direction())
668 .flex_none()
669 .children(buttons)
670 .semantic_in(cx, NodeSpec::new(parent, Role::Toolbar))
671 }
672}