1use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, ElementId, IntoElement, SharedString, Window};
5
6use super::ClickHandler;
7use crate::devtools::Probed;
8use crate::reactive::Binding;
9use crate::theme::{theme, ColorName, Size};
10
11#[derive(IntoElement)]
13pub struct Switch {
14 id: ElementId,
15 checked: bool,
16 label: Option<SharedString>,
17 size: Size,
18 color: ColorName,
19 disabled: bool,
20 binding: Option<Binding<bool>>,
21 on_change: Option<ClickHandler>,
22}
23
24impl Switch {
25 pub fn new(id: impl Into<ElementId>) -> Self {
26 Switch {
27 id: id.into(),
28 checked: false,
29 label: None,
30 size: Size::Md,
31 color: ColorName::Blue,
32 disabled: false,
33 binding: None,
34 on_change: None,
35 }
36 }
37
38 pub fn checked(mut self, checked: bool) -> Self {
39 self.checked = checked;
40 self
41 }
42
43 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
44 self.label = Some(label.into());
45 self
46 }
47
48 pub fn size(mut self, size: Size) -> Self {
49 self.size = size;
50 self
51 }
52
53 pub fn color(mut self, color: ColorName) -> Self {
54 self.color = color;
55 self
56 }
57
58 pub fn disabled(mut self, disabled: bool) -> Self {
59 self.disabled = disabled;
60 self
61 }
62
63 pub fn bind(mut self, binding: Binding<bool>) -> Self {
66 self.binding = Some(binding);
67 self
68 }
69
70 pub fn on_change(
71 mut self,
72 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
73 ) -> Self {
74 self.on_change = Some(Box::new(handler));
75 self
76 }
77
78 fn track_height(&self) -> f32 {
79 match self.size {
80 Size::Xs => 16.0,
81 Size::Sm => 20.0,
82 Size::Md => 24.0,
83 Size::Lg => 30.0,
84 Size::Xl => 36.0,
85 }
86 }
87}
88
89impl RenderOnce for Switch {
90 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
91 let t = theme(cx);
92 let height = self.track_height();
93 let width = (height * 1.85).round();
94 let knob = height - 4.0;
95 let accent = t.color(self.color, t.primary_shade());
96 let checked = self.binding.as_ref().map_or(self.checked, |b| b.get(cx));
97
98 let track_bg = if checked {
99 accent.hsla()
100 } else {
101 t.color(ColorName::Gray, if t.scheme.is_dark() { 6 } else { 4 })
102 .hsla()
103 };
104 let knob_x = if checked { width - knob - 2.0 } else { 2.0 };
105
106 let track = div()
107 .w(px(width))
108 .h(px(height))
109 .rounded(px(height))
110 .bg(track_bg)
111 .relative()
112 .child(
113 div()
114 .absolute()
115 .top(px(2.0))
116 .left(px(knob_x))
117 .w(px(knob))
118 .h(px(knob))
119 .rounded(px(knob))
120 .bg(t.white.hsla()),
121 );
122
123 let mut row = div()
124 .id(self.id)
125 .flex()
126 .items_center()
127 .gap(px(8.0))
128 .child(track);
129 if let Some(label) = self.label {
130 row = row.child(
131 div()
132 .text_size(px(t.font_size(self.size)))
133 .text_color(t.text().hsla())
134 .child(label),
135 );
136 }
137
138 let element = if self.disabled {
139 row.opacity(0.5)
140 } else {
141 if self.binding.is_some() || self.on_change.is_some() {
142 let binding = self.binding;
143 let handler = self.on_change;
144 let next = !checked;
145 row = row.on_click(move |ev, window, cx| {
146 if let Some(binding) = &binding {
147 binding.set(cx, next);
148 }
149 if let Some(handler) = &handler {
150 handler(ev, window, cx);
151 }
152 });
153 }
154 row
155 };
156
157 element
158 .probe("Switch")
159 .attr("size", self.size.label())
160 .attr_if("checked", self.checked)
161 .attr_if("disabled", self.disabled)
162 }
163}