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