Skip to main content

guise/input/
switch.rs

1//! `Switch` — a controlled on/off toggle styled as a sliding track.
2
3use 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/// A controlled switch.
12#[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    /// Two-way bind the on/off state. Overrides `checked`; clicks write the
64    /// toggled value back through the binding, then run any `on_change`.
65    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}