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