1use std::rc::Rc;
2
3use gpui::{AnyView, ClickEvent, relative};
4
5use crate::{ButtonLike, ButtonLikeRounding, TintColor, Tooltip, prelude::*};
6
7/// The position of a [`ToggleButton`] within a group of buttons.
8#[derive(Debug, PartialEq, Eq, Clone, Copy)]
9pub struct ToggleButtonPosition {
10 /// The toggle button is one of the leftmost of the group.
11 leftmost: bool,
12 /// The toggle button is one of the rightmost of the group.
13 rightmost: bool,
14 /// The toggle button is one of the topmost of the group.
15 topmost: bool,
16 /// The toggle button is one of the bottommost of the group.
17 bottommost: bool,
18}
19
20impl ToggleButtonPosition {
21 pub const HORIZONTAL_FIRST: Self = Self {
22 leftmost: true,
23 ..Self::HORIZONTAL_MIDDLE
24 };
25 pub const HORIZONTAL_MIDDLE: Self = Self {
26 leftmost: false,
27 rightmost: false,
28 topmost: true,
29 bottommost: true,
30 };
31 pub const HORIZONTAL_LAST: Self = Self {
32 rightmost: true,
33 ..Self::HORIZONTAL_MIDDLE
34 };
35
36 pub(crate) fn to_rounding(self) -> ButtonLikeRounding {
37 ButtonLikeRounding {
38 top_left: self.topmost && self.leftmost,
39 top_right: self.topmost && self.rightmost,
40 bottom_right: self.bottommost && self.rightmost,
41 bottom_left: self.bottommost && self.leftmost,
42 }
43 }
44}
45
46pub struct ButtonConfiguration {
47 label: SharedString,
48 icon: Option<IconName>,
49 on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
50 selected: bool,
51 tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
52}
53
54mod private {
55 pub trait ToggleButtonStyle {}
56}
57
58pub trait ButtonBuilder: 'static + private::ToggleButtonStyle {
59 fn into_configuration(self) -> ButtonConfiguration;
60}
61
62pub struct ToggleButtonSimple {
63 label: SharedString,
64 on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
65 selected: bool,
66 tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
67}
68
69impl ToggleButtonSimple {
70 pub fn new(
71 label: impl Into<SharedString>,
72 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
73 ) -> Self {
74 Self {
75 label: label.into(),
76 on_click: Box::new(on_click),
77 selected: false,
78 tooltip: None,
79 }
80 }
81
82 pub fn selected(mut self, selected: bool) -> Self {
83 self.selected = selected;
84 self
85 }
86
87 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
88 self.tooltip = Some(Rc::new(tooltip));
89 self
90 }
91}
92
93impl private::ToggleButtonStyle for ToggleButtonSimple {}
94
95impl ButtonBuilder for ToggleButtonSimple {
96 fn into_configuration(self) -> ButtonConfiguration {
97 ButtonConfiguration {
98 label: self.label,
99 icon: None,
100 on_click: self.on_click,
101 selected: self.selected,
102 tooltip: self.tooltip,
103 }
104 }
105}
106
107pub struct ToggleButtonWithIcon {
108 label: SharedString,
109 icon: IconName,
110 on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
111 selected: bool,
112 tooltip: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
113}
114
115impl ToggleButtonWithIcon {
116 pub fn new(
117 label: impl Into<SharedString>,
118 icon: IconName,
119 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
120 ) -> Self {
121 Self {
122 label: label.into(),
123 icon,
124 on_click: Box::new(on_click),
125 selected: false,
126 tooltip: None,
127 }
128 }
129
130 pub fn selected(mut self, selected: bool) -> Self {
131 self.selected = selected;
132 self
133 }
134
135 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
136 self.tooltip = Some(Rc::new(tooltip));
137 self
138 }
139}
140
141impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
142
143impl ButtonBuilder for ToggleButtonWithIcon {
144 fn into_configuration(self) -> ButtonConfiguration {
145 ButtonConfiguration {
146 label: self.label,
147 icon: Some(self.icon),
148 on_click: self.on_click,
149 selected: self.selected,
150 tooltip: self.tooltip,
151 }
152 }
153}
154
155#[derive(Clone, Copy, PartialEq)]
156pub enum ToggleButtonGroupStyle {
157 Transparent,
158 Filled,
159 Outlined,
160}
161
162#[derive(Clone, Copy, PartialEq)]
163pub enum ToggleButtonGroupSize {
164 Default,
165 Medium,
166 Large,
167 Custom(Rems),
168}
169
170#[derive(IntoElement)]
171pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
172where
173 T: ButtonBuilder,
174{
175 group_name: SharedString,
176 rows: [[T; COLS]; ROWS],
177 style: ToggleButtonGroupStyle,
178 size: ToggleButtonGroupSize,
179 label_size: LabelSize,
180 group_width: Option<DefiniteLength>,
181 auto_width: bool,
182 selected_index: usize,
183 tab_index: Option<isize>,
184}
185
186impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
187 pub fn single_row(group_name: impl Into<SharedString>, buttons: [T; COLS]) -> Self {
188 Self {
189 group_name: group_name.into(),
190 rows: [buttons],
191 style: ToggleButtonGroupStyle::Transparent,
192 size: ToggleButtonGroupSize::Default,
193 label_size: LabelSize::Small,
194 group_width: None,
195 auto_width: false,
196 selected_index: 0,
197 tab_index: None,
198 }
199 }
200}
201
202impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
203 pub fn two_rows(
204 group_name: impl Into<SharedString>,
205 first_row: [T; COLS],
206 second_row: [T; COLS],
207 ) -> Self {
208 Self {
209 group_name: group_name.into(),
210 rows: [first_row, second_row],
211 style: ToggleButtonGroupStyle::Transparent,
212 size: ToggleButtonGroupSize::Default,
213 label_size: LabelSize::Small,
214 group_width: None,
215 auto_width: false,
216 selected_index: 0,
217 tab_index: None,
218 }
219 }
220}
221
222impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> ToggleButtonGroup<T, COLS, ROWS> {
223 pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self {
224 self.style = style;
225 self
226 }
227
228 pub fn size(mut self, size: ToggleButtonGroupSize) -> Self {
229 self.size = size;
230 self
231 }
232
233 pub fn selected_index(mut self, index: usize) -> Self {
234 self.selected_index = index;
235 self
236 }
237
238 /// Makes the button group size itself to fit the content of the buttons,
239 /// rather than filling the full width of its parent.
240 pub fn auto_width(mut self) -> Self {
241 self.auto_width = true;
242 self
243 }
244
245 pub fn label_size(mut self, label_size: LabelSize) -> Self {
246 self.label_size = label_size;
247 self
248 }
249
250 /// Sets the tab index for the toggle button group.
251 /// The tab index is set to the initial value provided, then the
252 /// value is incremented by the number of buttons in the group.
253 pub fn tab_index(mut self, tab_index: &mut isize) -> Self {
254 self.tab_index = Some(*tab_index);
255 *tab_index += (COLS * ROWS) as isize;
256 self
257 }
258
259 const fn button_width() -> DefiniteLength {
260 relative(1. / COLS as f32)
261 }
262}
263
264impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> FixedWidth
265 for ToggleButtonGroup<T, COLS, ROWS>
266{
267 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
268 self.group_width = Some(width.into());
269 self
270 }
271
272 fn full_width(mut self) -> Self {
273 self.group_width = Some(relative(1.));
274 self
275 }
276}
277
278impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
279 for ToggleButtonGroup<T, COLS, ROWS>
280{
281 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
282 let custom_height = match self.size {
283 ToggleButtonGroupSize::Custom(height) => Some(height),
284 _ => None,
285 };
286
287 let entries =
288 self.rows.into_iter().enumerate().map(|(row_index, row)| {
289 let group_name = self.group_name.clone();
290 row.into_iter().enumerate().map(move |(col_index, button)| {
291 let ButtonConfiguration {
292 label,
293 icon,
294 on_click,
295 selected,
296 tooltip,
297 } = button.into_configuration();
298
299 let entry_index = row_index * COLS + col_index;
300
301 ButtonLike::new((group_name.clone(), entry_index))
302 .when(!self.auto_width, |this| this.full_width())
303 .rounding(Some(
304 ToggleButtonPosition {
305 leftmost: col_index == 0,
306 rightmost: col_index == COLS - 1,
307 topmost: row_index == 0,
308 bottommost: row_index == ROWS - 1,
309 }
310 .to_rounding(),
311 ))
312 .when_some(self.tab_index, |this, tab_index| {
313 this.tab_index(tab_index + entry_index as isize)
314 })
315 .when(entry_index == self.selected_index || selected, |this| {
316 this.toggle_state(true)
317 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
318 })
319 .when(self.style == ToggleButtonGroupStyle::Filled, |button| {
320 button.style(ButtonStyle::Filled)
321 })
322 .when(self.size == ToggleButtonGroupSize::Medium, |button| {
323 button.size(ButtonSize::Medium)
324 })
325 .when(self.size == ToggleButtonGroupSize::Large, |button| {
326 button.size(ButtonSize::Large)
327 })
328 .when_some(custom_height, |button, height| button.height(height.into()))
329 .child(
330 h_flex()
331 .w_full()
332 .px_2()
333 .gap_1p5()
334 .justify_center()
335 .flex_none()
336 .when_some(icon, |this, icon| {
337 this.py_2()
338 .child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
339 if entry_index == self.selected_index || selected {
340 this.color(Color::Accent)
341 } else {
342 this.color(Color::Muted)
343 }
344 }))
345 })
346 .child(Label::new(label).size(self.label_size).when(
347 entry_index == self.selected_index || selected,
348 |this| this.color(Color::Accent),
349 )),
350 )
351 .when_some(tooltip, |this, tooltip| {
352 this.tooltip(move |window, cx| tooltip(window, cx))
353 })
354 .on_click(on_click)
355 .into_any_element()
356 })
357 });
358
359 let border_color = cx.theme().colors().border.opacity(0.6);
360 let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined
361 || self.style == ToggleButtonGroupStyle::Filled;
362 let is_transparent = self.style == ToggleButtonGroupStyle::Transparent;
363
364 v_flex()
365 .map(|this| {
366 if let Some(width) = self.group_width {
367 this.w(width)
368 } else if self.auto_width {
369 this
370 } else {
371 this.w_full()
372 }
373 })
374 .rounded_md()
375 .overflow_hidden()
376 .map(|this| {
377 if is_transparent {
378 this.gap_px()
379 } else {
380 this.border_1().border_color(border_color)
381 }
382 })
383 .children(entries.enumerate().map(|(row_index, row)| {
384 let last_row = row_index == ROWS - 1;
385 h_flex()
386 .when(!is_outlined_or_filled, |this| this.gap_px())
387 .when(is_outlined_or_filled && !last_row, |this| {
388 this.border_b_1().border_color(border_color)
389 })
390 .children(row.enumerate().map(|(item_index, item)| {
391 let last_item = item_index == COLS - 1;
392 div()
393 .when(is_outlined_or_filled && !last_item, |this| {
394 this.border_r_1().border_color(border_color)
395 })
396 .when(!self.auto_width, |this| this.w(Self::button_width()))
397 .overflow_hidden()
398 .child(item)
399 }))
400 }))
401 }
402}
403
404fn register_toggle_button_group() {
405 component::register_component::<ToggleButtonGroup<ToggleButtonSimple>>();
406}
407
408component::__private::inventory::submit! {
409 component::ComponentFn::new(register_toggle_button_group)
410}
411
412impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> Component
413 for ToggleButtonGroup<T, COLS, ROWS>
414{
415 fn name() -> &'static str {
416 "ToggleButtonGroup"
417 }
418
419 fn scope() -> ComponentScope {
420 ComponentScope::Input
421 }
422
423 fn sort_name() -> &'static str {
424 "ButtonG"
425 }
426
427 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
428 Some(
429 v_flex()
430 .gap_6()
431 .children(vec![example_group_with_title(
432 "Transparent Variant",
433 vec![
434 single_example(
435 "Single Row Group",
436 ToggleButtonGroup::single_row(
437 "single_row_test",
438 [
439 ToggleButtonSimple::new("First", |_, _, _| {}),
440 ToggleButtonSimple::new("Second", |_, _, _| {}),
441 ToggleButtonSimple::new("Third", |_, _, _| {}),
442 ],
443 )
444 .selected_index(1)
445 .into_any_element(),
446 ),
447 single_example(
448 "Single Row Group with icons",
449 ToggleButtonGroup::single_row(
450 "single_row_test_icon",
451 [
452 ToggleButtonWithIcon::new(
453 "First",
454 IconName::AiZed,
455 |_, _, _| {},
456 ),
457 ToggleButtonWithIcon::new(
458 "Second",
459 IconName::AiZed,
460 |_, _, _| {},
461 ),
462 ToggleButtonWithIcon::new(
463 "Third",
464 IconName::AiZed,
465 |_, _, _| {},
466 ),
467 ],
468 )
469 .selected_index(1)
470 .into_any_element(),
471 ),
472 single_example(
473 "Multiple Row Group",
474 ToggleButtonGroup::two_rows(
475 "multiple_row_test",
476 [
477 ToggleButtonSimple::new("First", |_, _, _| {}),
478 ToggleButtonSimple::new("Second", |_, _, _| {}),
479 ToggleButtonSimple::new("Third", |_, _, _| {}),
480 ],
481 [
482 ToggleButtonSimple::new("Fourth", |_, _, _| {}),
483 ToggleButtonSimple::new("Fifth", |_, _, _| {}),
484 ToggleButtonSimple::new("Sixth", |_, _, _| {}),
485 ],
486 )
487 .selected_index(3)
488 .into_any_element(),
489 ),
490 single_example(
491 "Multiple Row Group with Icons",
492 ToggleButtonGroup::two_rows(
493 "multiple_row_test_icons",
494 [
495 ToggleButtonWithIcon::new(
496 "First",
497 IconName::AiZed,
498 |_, _, _| {},
499 ),
500 ToggleButtonWithIcon::new(
501 "Second",
502 IconName::AiZed,
503 |_, _, _| {},
504 ),
505 ToggleButtonWithIcon::new(
506 "Third",
507 IconName::AiZed,
508 |_, _, _| {},
509 ),
510 ],
511 [
512 ToggleButtonWithIcon::new(
513 "Fourth",
514 IconName::AiZed,
515 |_, _, _| {},
516 ),
517 ToggleButtonWithIcon::new(
518 "Fifth",
519 IconName::AiZed,
520 |_, _, _| {},
521 ),
522 ToggleButtonWithIcon::new(
523 "Sixth",
524 IconName::AiZed,
525 |_, _, _| {},
526 ),
527 ],
528 )
529 .selected_index(3)
530 .into_any_element(),
531 ),
532 ],
533 )])
534 .children(vec![example_group_with_title(
535 "Outlined Variant",
536 vec![
537 single_example(
538 "Single Row Group",
539 ToggleButtonGroup::single_row(
540 "single_row_test_outline",
541 [
542 ToggleButtonSimple::new("First", |_, _, _| {}),
543 ToggleButtonSimple::new("Second", |_, _, _| {}),
544 ToggleButtonSimple::new("Third", |_, _, _| {}),
545 ],
546 )
547 .selected_index(1)
548 .style(ToggleButtonGroupStyle::Outlined)
549 .into_any_element(),
550 ),
551 single_example(
552 "Single Row Group with icons",
553 ToggleButtonGroup::single_row(
554 "single_row_test_icon_outlined",
555 [
556 ToggleButtonWithIcon::new(
557 "First",
558 IconName::AiZed,
559 |_, _, _| {},
560 ),
561 ToggleButtonWithIcon::new(
562 "Second",
563 IconName::AiZed,
564 |_, _, _| {},
565 ),
566 ToggleButtonWithIcon::new(
567 "Third",
568 IconName::AiZed,
569 |_, _, _| {},
570 ),
571 ],
572 )
573 .selected_index(1)
574 .style(ToggleButtonGroupStyle::Outlined)
575 .into_any_element(),
576 ),
577 single_example(
578 "Multiple Row Group",
579 ToggleButtonGroup::two_rows(
580 "multiple_row_test",
581 [
582 ToggleButtonSimple::new("First", |_, _, _| {}),
583 ToggleButtonSimple::new("Second", |_, _, _| {}),
584 ToggleButtonSimple::new("Third", |_, _, _| {}),
585 ],
586 [
587 ToggleButtonSimple::new("Fourth", |_, _, _| {}),
588 ToggleButtonSimple::new("Fifth", |_, _, _| {}),
589 ToggleButtonSimple::new("Sixth", |_, _, _| {}),
590 ],
591 )
592 .selected_index(3)
593 .style(ToggleButtonGroupStyle::Outlined)
594 .into_any_element(),
595 ),
596 single_example(
597 "Multiple Row Group with Icons",
598 ToggleButtonGroup::two_rows(
599 "multiple_row_test",
600 [
601 ToggleButtonWithIcon::new(
602 "First",
603 IconName::AiZed,
604 |_, _, _| {},
605 ),
606 ToggleButtonWithIcon::new(
607 "Second",
608 IconName::AiZed,
609 |_, _, _| {},
610 ),
611 ToggleButtonWithIcon::new(
612 "Third",
613 IconName::AiZed,
614 |_, _, _| {},
615 ),
616 ],
617 [
618 ToggleButtonWithIcon::new(
619 "Fourth",
620 IconName::AiZed,
621 |_, _, _| {},
622 ),
623 ToggleButtonWithIcon::new(
624 "Fifth",
625 IconName::AiZed,
626 |_, _, _| {},
627 ),
628 ToggleButtonWithIcon::new(
629 "Sixth",
630 IconName::AiZed,
631 |_, _, _| {},
632 ),
633 ],
634 )
635 .selected_index(3)
636 .style(ToggleButtonGroupStyle::Outlined)
637 .into_any_element(),
638 ),
639 ],
640 )])
641 .children(vec![example_group_with_title(
642 "Filled Variant",
643 vec![
644 single_example(
645 "Single Row Group",
646 ToggleButtonGroup::single_row(
647 "single_row_test_outline",
648 [
649 ToggleButtonSimple::new("First", |_, _, _| {}),
650 ToggleButtonSimple::new("Second", |_, _, _| {}),
651 ToggleButtonSimple::new("Third", |_, _, _| {}),
652 ],
653 )
654 .selected_index(2)
655 .style(ToggleButtonGroupStyle::Filled)
656 .into_any_element(),
657 ),
658 single_example(
659 "Single Row Group with icons",
660 ToggleButtonGroup::single_row(
661 "single_row_test_icon_outlined",
662 [
663 ToggleButtonWithIcon::new(
664 "First",
665 IconName::AiZed,
666 |_, _, _| {},
667 ),
668 ToggleButtonWithIcon::new(
669 "Second",
670 IconName::AiZed,
671 |_, _, _| {},
672 ),
673 ToggleButtonWithIcon::new(
674 "Third",
675 IconName::AiZed,
676 |_, _, _| {},
677 ),
678 ],
679 )
680 .selected_index(1)
681 .style(ToggleButtonGroupStyle::Filled)
682 .into_any_element(),
683 ),
684 single_example(
685 "Multiple Row Group",
686 ToggleButtonGroup::two_rows(
687 "multiple_row_test",
688 [
689 ToggleButtonSimple::new("First", |_, _, _| {}),
690 ToggleButtonSimple::new("Second", |_, _, _| {}),
691 ToggleButtonSimple::new("Third", |_, _, _| {}),
692 ],
693 [
694 ToggleButtonSimple::new("Fourth", |_, _, _| {}),
695 ToggleButtonSimple::new("Fifth", |_, _, _| {}),
696 ToggleButtonSimple::new("Sixth", |_, _, _| {}),
697 ],
698 )
699 .selected_index(3)
700 .width(rems_from_px(100.))
701 .style(ToggleButtonGroupStyle::Filled)
702 .into_any_element(),
703 ),
704 single_example(
705 "Multiple Row Group with Icons",
706 ToggleButtonGroup::two_rows(
707 "multiple_row_test",
708 [
709 ToggleButtonWithIcon::new(
710 "First",
711 IconName::AiZed,
712 |_, _, _| {},
713 ),
714 ToggleButtonWithIcon::new(
715 "Second",
716 IconName::AiZed,
717 |_, _, _| {},
718 ),
719 ToggleButtonWithIcon::new(
720 "Third",
721 IconName::AiZed,
722 |_, _, _| {},
723 ),
724 ],
725 [
726 ToggleButtonWithIcon::new(
727 "Fourth",
728 IconName::AiZed,
729 |_, _, _| {},
730 ),
731 ToggleButtonWithIcon::new(
732 "Fifth",
733 IconName::AiZed,
734 |_, _, _| {},
735 ),
736 ToggleButtonWithIcon::new(
737 "Sixth",
738 IconName::AiZed,
739 |_, _, _| {},
740 ),
741 ],
742 )
743 .selected_index(3)
744 .width(rems_from_px(100.))
745 .style(ToggleButtonGroupStyle::Filled)
746 .into_any_element(),
747 ),
748 ],
749 )])
750 .children(vec![single_example(
751 "With Tooltips",
752 ToggleButtonGroup::single_row(
753 "with_tooltips",
754 [
755 ToggleButtonSimple::new("First", |_, _, _| {})
756 .tooltip(Tooltip::text("This is a tooltip. Hello!")),
757 ToggleButtonSimple::new("Second", |_, _, _| {})
758 .tooltip(Tooltip::text("This is a tooltip. Hey?")),
759 ToggleButtonSimple::new("Third", |_, _, _| {})
760 .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")),
761 ],
762 )
763 .selected_index(1)
764 .into_any_element(),
765 )])
766 .into_any_element(),
767 )
768 }
769}