Skip to main content

rgpui_component/
switch.rs

1use crate::{
2    ActiveTheme, Disableable, Side, Sizable, Size, StyledExt, h_flex, text::Text,
3    tooltip::ComponentTooltip,
4};
5use rgpui::{
6    Animation, AnimationExt as _, App, ElementId, Hsla, InteractiveElement, IntoElement,
7    ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled, Window, div,
8    prelude::FluentBuilder as _, px,
9};
10use std::{rc::Rc, time::Duration};
11
12/// A Switch element that can be toggled on or off.
13#[derive(IntoElement)]
14pub struct Switch {
15    id: ElementId,
16    style: StyleRefinement,
17    checked: bool,
18    disabled: bool,
19    label: Option<Text>,
20    label_side: Side,
21    on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
22    size: Size,
23    color: Option<Hsla>,
24    tooltip: ComponentTooltip,
25}
26
27impl Switch {
28    /// Create a new Switch element.
29    pub fn new(id: impl Into<ElementId>) -> Self {
30        let id: ElementId = id.into();
31        Self {
32            id: id.clone(),
33            style: StyleRefinement::default(),
34            checked: false,
35            disabled: false,
36            label: None,
37            on_click: None,
38            label_side: Side::Right,
39            size: Size::Medium,
40            color: None,
41            tooltip: ComponentTooltip::default(),
42        }
43    }
44
45    /// Set the checked state of the switch.
46    pub fn checked(mut self, checked: bool) -> Self {
47        self.checked = checked;
48        self
49    }
50
51    /// Set the label of the switch.
52    pub fn label(mut self, label: impl Into<Text>) -> Self {
53        self.label = Some(label.into());
54        self
55    }
56
57    /// Add a click handler for the switch.
58    pub fn on_click<F>(mut self, handler: F) -> Self
59    where
60        F: Fn(&bool, &mut Window, &mut App) + 'static,
61    {
62        self.on_click = Some(Rc::new(handler));
63        self
64    }
65
66    /// Set the background color of the switch when checked.
67    /// Defaults to `cx.theme().primary`.
68    pub fn color(mut self, color: impl Into<Hsla>) -> Self {
69        self.color = Some(color.into());
70        self
71    }
72
73    /// Set tooltip text for the switch.
74    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
75        self.tooltip.text = Some((tooltip.into(), None));
76        self
77    }
78}
79
80impl Styled for Switch {
81    fn style(&mut self) -> &mut rgpui::StyleRefinement {
82        &mut self.style
83    }
84}
85
86impl Sizable for Switch {
87    fn with_size(mut self, size: impl Into<Size>) -> Self {
88        self.size = size.into();
89        self
90    }
91}
92
93impl Disableable for Switch {
94    fn disabled(mut self, disabled: bool) -> Self {
95        self.disabled = disabled;
96        self
97    }
98}
99
100impl RenderOnce for Switch {
101    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
102        let checked = self.checked;
103        let on_click = self.on_click.clone();
104        let toggle_state = window.use_keyed_state(self.id.clone(), cx, |_, _| checked);
105
106        let checked_bg = self.color.unwrap_or(cx.theme().primary);
107        let (bg, toggle_bg) = match checked {
108            true => (checked_bg, cx.theme().switch_thumb),
109            false => (cx.theme().switch, cx.theme().switch_thumb),
110        };
111
112        let (bg, toggle_bg) = if self.disabled {
113            (
114                if checked { bg.alpha(0.5) } else { bg },
115                toggle_bg.alpha(0.35),
116            )
117        } else {
118            (bg, toggle_bg)
119        };
120
121        let (bg_width, bg_height) = match self.size {
122            Size::XSmall | Size::Small => (px(28.), px(16.)),
123            _ => (px(36.), px(20.)),
124        };
125        let bar_width = match self.size {
126            Size::XSmall | Size::Small => px(12.),
127            _ => px(16.),
128        };
129        let inset = px(2.);
130        let radius = if cx.theme().radius >= px(4.) {
131            bg_height
132        } else {
133            cx.theme().radius
134        };
135
136        div().refine_style(&self.style).child(
137            h_flex()
138                .id(self.id.clone())
139                .gap_2()
140                .items_start()
141                .when(self.label_side.is_left(), |this| this.flex_row_reverse())
142                .child(
143                    // Switch Bar
144                    div()
145                        .id(self.id.clone())
146                        .w(bg_width)
147                        .h(bg_height)
148                        .rounded(radius)
149                        .flex()
150                        .items_center()
151                        .border(inset)
152                        .border_color(cx.theme().transparent)
153                        .bg(bg)
154                        .map(|this| self.tooltip.apply(this))
155                        .child(
156                            // Switch Toggle
157                            div()
158                                .rounded(radius)
159                                .bg(toggle_bg)
160                                .shadow_md()
161                                .size(bar_width)
162                                .map(|this| {
163                                    let prev_checked = toggle_state.read(cx);
164                                    if !self.disabled && *prev_checked != checked {
165                                        let duration = Duration::from_secs_f64(0.15);
166                                        cx.spawn({
167                                            let toggle_state = toggle_state.clone();
168                                            async move |cx| {
169                                                cx.background_executor().timer(duration).await;
170                                                _ = toggle_state
171                                                    .update(cx, |this, _| *this = checked);
172                                            }
173                                        })
174                                        .detach();
175
176                                        this.with_animation(
177                                            ElementId::NamedInteger("move".into(), checked as u64),
178                                            Animation::new(duration),
179                                            move |this, delta| {
180                                                let max_x = bg_width - bar_width - inset * 2;
181                                                let x = if checked {
182                                                    max_x * delta
183                                                } else {
184                                                    max_x - max_x * delta
185                                                };
186                                                this.left(x)
187                                            },
188                                        )
189                                        .into_any_element()
190                                    } else {
191                                        let max_x = bg_width - bar_width - inset * 2;
192                                        let x = if checked { max_x } else { px(0.) };
193                                        this.left(x).into_any_element()
194                                    }
195                                }),
196                        ),
197                )
198                .when_some(self.label, |this, label| {
199                    this.child(div().line_height(bg_height).child(label).map(
200                        |this| match self.size {
201                            Size::XSmall | Size::Small => this.text_sm(),
202                            _ => this.text_base(),
203                        },
204                    ))
205                })
206                .when_some(
207                    on_click
208                        .as_ref()
209                        .map(|c| c.clone())
210                        .filter(|_| !self.disabled),
211                    |this, on_click| {
212                        let toggle_state = toggle_state.clone();
213                        this.on_mouse_down(rgpui::MouseButton::Left, move |_, window, cx| {
214                            cx.stop_propagation();
215                            _ = toggle_state.update(cx, |this, _| *this = checked);
216                            on_click(&!checked, window, cx);
217                        })
218                    },
219                ),
220        )
221    }
222}