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#[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 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 pub fn checked(mut self, checked: bool) -> Self {
47 self.checked = checked;
48 self
49 }
50
51 pub fn label(mut self, label: impl Into<Text>) -> Self {
53 self.label = Some(label.into());
54 self
55 }
56
57 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 pub fn color(mut self, color: impl Into<Hsla>) -> Self {
69 self.color = Some(color.into());
70 self
71 }
72
73 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 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 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}