ccf_gpui_widgets/widgets/
toggle_switch.rs1use gpui::prelude::*;
26use gpui::*;
27
28use crate::theme::{get_theme_or, Theme};
29use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum LabelPosition {
34 Left,
36 #[default]
38 Right,
39}
40
41#[derive(Clone, Debug)]
43pub enum ToggleSwitchEvent {
44 Toggle(bool),
47}
48
49pub struct ToggleSwitch {
51 on: bool,
53 label: Option<SharedString>,
54 label_position: LabelPosition,
55 focus_handle: FocusHandle,
56 custom_theme: Option<Theme>,
57 enabled: bool,
59}
60
61impl EventEmitter<ToggleSwitchEvent> for ToggleSwitch {}
62
63impl Focusable for ToggleSwitch {
64 fn focus_handle(&self, _cx: &App) -> FocusHandle {
65 self.focus_handle.clone()
66 }
67}
68
69impl ToggleSwitch {
70 pub fn new(cx: &mut Context<Self>) -> Self {
72 Self {
73 on: false,
74 label: None,
75 label_position: LabelPosition::default(),
76 focus_handle: cx.focus_handle().tab_stop(true),
77 custom_theme: None,
78 enabled: true,
79 }
80 }
81
82 #[must_use]
84 pub fn with_on(mut self, value: bool) -> Self {
85 self.on = value;
86 self
87 }
88
89 #[must_use]
91 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
92 self.label = Some(text.into());
93 self
94 }
95
96 #[must_use]
98 pub fn label_position(mut self, position: LabelPosition) -> Self {
99 self.label_position = position;
100 self
101 }
102
103 #[must_use]
105 pub fn theme(mut self, theme: Theme) -> Self {
106 self.custom_theme = Some(theme);
107 self
108 }
109
110 #[must_use]
112 pub fn with_enabled(mut self, enabled: bool) -> Self {
113 self.enabled = enabled;
114 self
115 }
116
117 pub fn is_on(&self) -> bool {
119 self.on
120 }
121
122 pub fn set_on(&mut self, on: bool, cx: &mut Context<Self>) {
124 if self.on != on {
125 self.on = on;
126 cx.emit(ToggleSwitchEvent::Toggle(on));
127 cx.notify();
128 }
129 }
130
131 pub fn focus_handle(&self) -> &FocusHandle {
133 &self.focus_handle
134 }
135
136 pub fn is_enabled(&self) -> bool {
138 self.enabled
139 }
140
141 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
143 if self.enabled != enabled {
144 self.enabled = enabled;
145 cx.notify();
146 }
147 }
148
149 fn toggle(&mut self, cx: &mut Context<Self>) {
150 self.on = !self.on;
151 cx.emit(ToggleSwitchEvent::Toggle(self.on));
152 cx.notify();
153 }
154}
155
156impl Render for ToggleSwitch {
157 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
158 let theme = get_theme_or(cx, self.custom_theme.as_ref());
159 let is_on = self.on;
160 let label = self.label.clone();
161 let label_position = self.label_position;
162 let focus_handle = self.focus_handle.clone();
163 let is_focused = self.focus_handle.is_focused(window);
164 let enabled = self.enabled;
165
166 let track_width = 44.0;
168 let track_height = 24.0;
169 let thumb_size = 18.0;
170 let thumb_padding = 3.0;
171
172 let thumb_left = if is_on {
174 track_width - thumb_size - thumb_padding
175 } else {
176 thumb_padding
177 };
178
179 let make_label = |text: SharedString| {
181 div()
182 .text_sm()
183 .font_weight(FontWeight::SEMIBOLD)
184 .when(enabled, |d| d.text_color(rgb(theme.text_label)))
185 .when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
186 .child(text)
187 };
188
189 let make_toggle = || {
191 let (track_bg, thumb_bg) = if enabled {
192 let track = if is_on { theme.primary } else { theme.bg_input };
193 (track, theme.bg_white)
194 } else {
195 let track = if is_on { theme.disabled_text } else { theme.disabled_bg };
196 (track, theme.disabled_bg)
197 };
198
199 div()
200 .w(px(track_width))
201 .h(px(track_height))
202 .rounded(px(track_height / 2.0)) .relative()
204 .bg(rgb(track_bg))
205 .cursor_for_enabled(enabled)
206 .child(
207 div()
209 .absolute()
210 .top(px(thumb_padding))
211 .left(px(thumb_left))
212 .w(px(thumb_size))
213 .h(px(thumb_size))
214 .rounded_full()
215 .bg(rgb(thumb_bg))
216 .when(enabled, |d| d.shadow_sm())
217 )
218 };
219
220 let mut container = with_focus_actions(
221 div()
222 .id("ccf_toggle_switch")
223 .track_focus(&focus_handle)
224 .tab_stop(enabled),
225 cx,
226 )
227 .on_key_down(cx.listener(move |toggle, event: &KeyDownEvent, window, cx| {
228 if !toggle.enabled {
229 return;
230 }
231 if handle_tab_navigation(event, window) {
232 return;
233 }
234 if matches!(event.keystroke.key.as_str(), "space" | "enter") {
235 toggle.toggle(cx);
236 }
237 }))
238 .flex()
239 .flex_row()
240 .gap_2()
241 .items_center()
242 .py_1()
243 .px_1()
244 .rounded_sm()
245 .cursor_for_enabled(enabled)
246 .border_2()
247 .border_color(if is_focused && enabled { rgb(theme.border_focus) } else { rgba(0x00000000) });
248
249 if enabled {
250 container = container.on_mouse_down(MouseButton::Left, cx.listener(|toggle, _event, window, cx| {
251 toggle.focus_handle.focus(window);
252 toggle.toggle(cx);
253 }));
254 }
255
256 match (label_position, label) {
258 (LabelPosition::Left, Some(text)) => {
259 container = container.child(make_label(text)).child(make_toggle());
260 }
261 (LabelPosition::Right, Some(text)) => {
262 container = container.child(make_toggle()).child(make_label(text));
263 }
264 (_, None) => {
265 container = container.child(make_toggle());
266 }
267 }
268
269 container
270 }
271}