ui: Add ToggleButtonGroup component (#35118)

<img width="600" height="704" alt="CleanShot 2025-07-25 at 8  03 04@2x"
src="https://github.com/user-attachments/assets/3d2b29ba-e0fd-4231-bb80-746903d61481"
/>

Release Notes:

- N/A

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
This commit is contained in:
Danilo Leal 2025-07-25 20:32:53 -03:00 committed by GitHub
parent aea309160b
commit 6a79c4e50a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 651 additions and 38 deletions

View file

@ -1,6 +1,6 @@
use gpui::{AnyView, ClickEvent};
use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, prelude::*};
use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*};
/// The position of a [`ToggleButton`] within a group of buttons.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@ -290,3 +290,627 @@ impl Component for ToggleButton {
)
}
}
mod private {
pub trait Sealed {}
}
pub trait ButtonBuilder: 'static + private::Sealed {
fn label(&self) -> impl Into<SharedString>;
fn icon(&self) -> Option<IconName>;
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
}
pub struct ToggleButtonSimple {
label: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
}
impl ToggleButtonSimple {
pub fn new(
label: impl Into<SharedString>,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
Self {
label: label.into(),
on_click: Box::new(on_click),
}
}
}
impl private::Sealed for ToggleButtonSimple {}
impl ButtonBuilder for ToggleButtonSimple {
fn label(&self) -> impl Into<SharedString> {
self.label.clone()
}
fn icon(&self) -> Option<IconName> {
None
}
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
self.on_click
}
}
pub struct ToggleButtonWithIcon {
label: SharedString,
icon: IconName,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
}
impl ToggleButtonWithIcon {
pub fn new(
label: impl Into<SharedString>,
icon: IconName,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
Self {
label: label.into(),
icon,
on_click: Box::new(on_click),
}
}
}
impl private::Sealed for ToggleButtonWithIcon {}
impl ButtonBuilder for ToggleButtonWithIcon {
fn label(&self) -> impl Into<SharedString> {
self.label.clone()
}
fn icon(&self) -> Option<IconName> {
Some(self.icon)
}
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
self.on_click
}
}
struct ToggleButtonRow<T: ButtonBuilder> {
items: Vec<T>,
index_offset: usize,
last_item_idx: usize,
is_last_row: bool,
}
impl<T: ButtonBuilder> ToggleButtonRow<T> {
fn new(items: Vec<T>, index_offset: usize, is_last_row: bool) -> Self {
Self {
index_offset,
last_item_idx: index_offset + items.len() - 1,
is_last_row,
items,
}
}
}
enum ToggleButtonGroupRows<T: ButtonBuilder> {
Single(Vec<T>),
Multiple(Vec<T>, Vec<T>),
}
impl<T: ButtonBuilder> ToggleButtonGroupRows<T> {
fn items(self) -> impl IntoIterator<Item = ToggleButtonRow<T>> {
match self {
ToggleButtonGroupRows::Single(items) => {
vec![ToggleButtonRow::new(items, 0, true)]
}
ToggleButtonGroupRows::Multiple(first_row, second_row) => {
let row_len = first_row.len();
vec![
ToggleButtonRow::new(first_row, 0, false),
ToggleButtonRow::new(second_row, row_len, true),
]
}
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum ToggleButtonGroupStyle {
Transparent,
Filled,
Outlined,
}
#[derive(IntoElement)]
pub struct ToggleButtonGroup<T>
where
T: ButtonBuilder,
{
group_name: SharedString,
rows: ToggleButtonGroupRows<T>,
style: ToggleButtonGroupStyle,
button_width: Rems,
selected_index: usize,
}
impl<T: ButtonBuilder> ToggleButtonGroup<T> {
pub fn single_row(
group_name: impl Into<SharedString>,
buttons: impl IntoIterator<Item = T>,
) -> Self {
Self {
group_name: group_name.into(),
rows: ToggleButtonGroupRows::Single(Vec::from_iter(buttons)),
style: ToggleButtonGroupStyle::Transparent,
button_width: rems_from_px(100.),
selected_index: 0,
}
}
pub fn multiple_rows<const ROWS: usize>(
group_name: impl Into<SharedString>,
first_row: [T; ROWS],
second_row: [T; ROWS],
) -> Self {
Self {
group_name: group_name.into(),
rows: ToggleButtonGroupRows::Multiple(
Vec::from_iter(first_row),
Vec::from_iter(second_row),
),
style: ToggleButtonGroupStyle::Transparent,
button_width: rems_from_px(100.),
selected_index: 0,
}
}
pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self {
self.style = style;
self
}
pub fn button_width(mut self, button_width: Rems) -> Self {
self.button_width = button_width;
self
}
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = index;
self
}
}
impl<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let rows = self.rows.items().into_iter().map(|row| {
(
row.items
.into_iter()
.enumerate()
.map(move |(index, item)| (index + row.index_offset, row.last_item_idx, item))
.map(|(index, last_item_idx, item)| {
(
ButtonLike::new((self.group_name.clone(), index))
.when(index == self.selected_index, |this| {
this.toggle_state(true)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
})
.rounding(None)
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
button.style(ButtonStyle::Filled)
})
.child(
h_flex()
.min_w(self.button_width)
.gap_1p5()
.justify_center()
.when_some(item.icon(), |this, icon| {
this.child(Icon::new(icon).size(IconSize::XSmall).map(
|this| {
if index == self.selected_index {
this.color(Color::Accent)
} else {
this.color(Color::Muted)
}
},
))
})
.child(
Label::new(item.label())
.when(index == self.selected_index, |this| {
this.color(Color::Accent)
}),
),
)
.on_click(item.on_click()),
index == last_item_idx,
)
}),
row.is_last_row,
)
});
let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined
|| self.style == ToggleButtonGroupStyle::Filled;
let is_transparent = self.style == ToggleButtonGroupStyle::Transparent;
let border_color = cx.theme().colors().border.opacity(0.6);
v_flex()
.rounded_md()
.overflow_hidden()
.map(|this| {
if is_transparent {
this.gap_px()
} else {
this.border_1().border_color(border_color)
}
})
.children(rows.map(|(items, last_row)| {
h_flex()
.when(!is_outlined_or_filled, |this| this.gap_px())
.when(is_outlined_or_filled && !last_row, |this| {
this.border_b_1().border_color(border_color)
})
.children(items.map(|(item, last_item)| {
div()
.when(is_outlined_or_filled && !last_item, |this| {
this.border_r_1().border_color(border_color)
})
.child(item)
}))
}))
}
}
fn register_toggle_button_group() {
component::register_component::<ToggleButtonGroup<ToggleButtonSimple>>();
}
component::__private::inventory::submit! {
component::ComponentFn::new(register_toggle_button_group)
}
impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
fn name() -> &'static str {
"ToggleButtonGroup"
}
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
"ButtonG"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Transparent Variant",
vec![
single_example(
"Single Row Group",
ToggleButtonGroup::single_row(
"single_row_test",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
)
.selected_index(1)
.button_width(rems_from_px(100.))
.into_any_element(),
),
single_example(
"Single Row Group with icons",
ToggleButtonGroup::single_row(
"single_row_test_icon",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(1)
.button_width(rems_from_px(100.))
.into_any_element(),
),
single_example(
"Multiple Row Group",
ToggleButtonGroup::multiple_rows(
"multiple_row_test",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
[
ToggleButtonSimple::new("Fourth", |_, _, _| {}),
ToggleButtonSimple::new("Fifth", |_, _, _| {}),
ToggleButtonSimple::new("Sixth", |_, _, _| {}),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.into_any_element(),
),
single_example(
"Multiple Row Group with Icons",
ToggleButtonGroup::multiple_rows(
"multiple_row_test_icons",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
[
ToggleButtonWithIcon::new(
"Fourth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Fifth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Sixth",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.into_any_element(),
),
],
)])
.children(vec![example_group_with_title(
"Outlined Variant",
vec![
single_example(
"Single Row Group",
ToggleButtonGroup::single_row(
"single_row_test_outline",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
)
.selected_index(1)
.style(ToggleButtonGroupStyle::Outlined)
.into_any_element(),
),
single_example(
"Single Row Group with icons",
ToggleButtonGroup::single_row(
"single_row_test_icon_outlined",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(1)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Outlined)
.into_any_element(),
),
single_example(
"Multiple Row Group",
ToggleButtonGroup::multiple_rows(
"multiple_row_test",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
[
ToggleButtonSimple::new("Fourth", |_, _, _| {}),
ToggleButtonSimple::new("Fifth", |_, _, _| {}),
ToggleButtonSimple::new("Sixth", |_, _, _| {}),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Outlined)
.into_any_element(),
),
single_example(
"Multiple Row Group with Icons",
ToggleButtonGroup::multiple_rows(
"multiple_row_test",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
[
ToggleButtonWithIcon::new(
"Fourth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Fifth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Sixth",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Outlined)
.into_any_element(),
),
],
)])
.children(vec![example_group_with_title(
"Filled Variant",
vec![
single_example(
"Single Row Group",
ToggleButtonGroup::single_row(
"single_row_test_outline",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
)
.selected_index(2)
.style(ToggleButtonGroupStyle::Filled)
.into_any_element(),
),
single_example(
"Single Row Group with icons",
ToggleButtonGroup::single_row(
"single_row_test_icon_outlined",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(1)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Filled)
.into_any_element(),
),
single_example(
"Multiple Row Group",
ToggleButtonGroup::multiple_rows(
"multiple_row_test",
[
ToggleButtonSimple::new("First", |_, _, _| {}),
ToggleButtonSimple::new("Second", |_, _, _| {}),
ToggleButtonSimple::new("Third", |_, _, _| {}),
],
[
ToggleButtonSimple::new("Fourth", |_, _, _| {}),
ToggleButtonSimple::new("Fifth", |_, _, _| {}),
ToggleButtonSimple::new("Sixth", |_, _, _| {}),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Filled)
.into_any_element(),
),
single_example(
"Multiple Row Group with Icons",
ToggleButtonGroup::multiple_rows(
"multiple_row_test",
[
ToggleButtonWithIcon::new(
"First",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Second",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Third",
IconName::AiZed,
|_, _, _| {},
),
],
[
ToggleButtonWithIcon::new(
"Fourth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Fifth",
IconName::AiZed,
|_, _, _| {},
),
ToggleButtonWithIcon::new(
"Sixth",
IconName::AiZed,
|_, _, _| {},
),
],
)
.selected_index(3)
.button_width(rems_from_px(100.))
.style(ToggleButtonGroupStyle::Filled)
.into_any_element(),
),
],
)])
.into_any_element(),
)
}
}