use std::{cell::Cell, rc::Rc}; use gpui::{ div, prelude::FluentBuilder as _, AnyElement, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, }; use smallvec::{smallvec, SmallVec}; use crate::{h_flex, ActiveTheme, Disableable, Icon, Sizable, Size, StyledExt}; #[derive(Default, Copy, Debug, Clone, PartialEq, Eq, Hash)] pub enum ToggleVariant { #[default] Ghost, Outline, } pub trait ToggleVariants: Sized { /// Set the variant of the toggle. fn with_variant(self, variant: ToggleVariant) -> Self; /// Set the variant to ghost. fn ghost(self) -> Self { self.with_variant(ToggleVariant::Ghost) } /// Set the variant to outline. fn outline(self) -> Self { self.with_variant(ToggleVariant::Outline) } } #[derive(IntoElement)] pub struct Toggle { id: ElementId, style: StyleRefinement, checked: bool, size: Size, variant: ToggleVariant, disabled: bool, children: SmallVec<[AnyElement; 1]>, on_click: Option>, } impl Toggle { /// Create a new Toggle element. pub fn new(id: impl Into) -> Self { Self { id: id.into(), style: StyleRefinement::default(), checked: false, size: Size::default(), variant: ToggleVariant::default(), disabled: false, children: smallvec![], on_click: None, } } /// Add a label to the toggle. pub fn label(mut self, label: impl Into) -> Self { let label: SharedString = label.into(); self.children.push(label.into_any_element()); self } /// Add icon to the toggle. pub fn icon(mut self, icon: impl Into) -> Self { let icon: Icon = icon.into(); self.children.push(icon.into()); self } /// Set the checked state of the toggle, default: false pub fn checked(mut self, checked: bool) -> Self { self.checked = checked; self } /// Set the callback to be called when the toggle is clicked. /// /// The `&bool` parameter represents the new checked state of the toggle. pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self } } impl ToggleVariants for Toggle { fn with_variant(mut self, variant: ToggleVariant) -> Self { self.variant = variant; self } } impl ParentElement for Toggle { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Disableable for Toggle { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl Sizable for Toggle { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Styled for Toggle { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for Toggle { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let checked = self.checked; let disabled = self.disabled; let hoverable = !disabled && !checked; div() .id(self.id) .flex() .flex_row() .items_center() .justify_center() .map(|this| match self.size { Size::XSmall => this.min_w_5().h_5().px_0p5().text_xs(), Size::Small => this.min_w_6().h_6().px_1().text_sm(), Size::Large => this.min_w_9().h_9().px_3().text_lg(), _ => this.min_w_8().h_8().px_2(), }) .rounded(cx.theme().radius) .when(self.variant == ToggleVariant::Outline, |this| { this.border_1() .border_color(cx.theme().border) .bg(cx.theme().background) .when(cx.theme().shadow, |this| this.shadow_xs()) }) .when(hoverable, |this| { this.hover(|this| { this.bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) }) }) .when(checked, |this| { this.bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) }) .refine_style(&self.style) .children(self.children) .when(!disabled, |this| { this.when_some(self.on_click, |this, on_click| { this.on_click(move |_, window, cx| on_click(&!checked, window, cx)) }) }) } } /// A group of toggles. #[derive(IntoElement)] pub struct ToggleGroup { id: ElementId, style: StyleRefinement, size: Size, variant: ToggleVariant, disabled: bool, items: Vec, on_click: Option, &mut Window, &mut App) + 'static>>, } impl ToggleGroup { /// Create a new ToggleGroup element. pub fn new(id: impl Into) -> Self { Self { id: id.into(), style: StyleRefinement::default(), size: Size::default(), variant: ToggleVariant::default(), disabled: false, items: Vec::new(), on_click: None, } } /// Add a child [`Toggle`] to the group. pub fn child(mut self, toggle: impl Into) -> Self { self.items.push(toggle.into()); self } /// Add multiple [`Toggle`]s to the group. pub fn children(mut self, children: impl IntoIterator>) -> Self { self.items.extend(children.into_iter().map(Into::into)); self } /// Set the callback to be called when the toggle group changes. /// /// The `&Vec` parameter represents the new check state of each [`Toggle`] in the group. pub fn on_click( mut self, on_click: impl Fn(&Vec, &mut Window, &mut App) + 'static, ) -> Self { self.on_click = Some(Rc::new(on_click)); self } } impl Sizable for ToggleGroup { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl ToggleVariants for ToggleGroup { fn with_variant(mut self, variant: ToggleVariant) -> Self { self.variant = variant; self } } impl Disableable for ToggleGroup { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl Styled for ToggleGroup { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for ToggleGroup { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let disabled = self.disabled; let checks = self .items .iter() .map(|item| item.checked) .collect::>(); let state = Rc::new(Cell::new(None)); h_flex() .id(self.id) .gap_1() .refine_style(&self.style) .children(self.items.into_iter().enumerate().map({ |(ix, item)| { let state = state.clone(); item.disabled(disabled) .with_size(self.size) .with_variant(self.variant) .on_click(move |_, _, _| { state.set(Some(ix)); }) } })) .when(!disabled, |this| { this.when_some(self.on_click, |this, on_click| { this.on_click(move |_, window, cx| { if let Some(ix) = state.get() { let mut checks = checks.clone(); checks[ix] = !checks[ix]; on_click(&checks, window, cx); } }) }) }) } }