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::reactive::Binding;
8use crate::theme::{theme, ColorName, Size};
9
10/// A controlled switch. The Mantine `Switch`.
11#[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    /// Two-way bind the on/off state. Overrides `checked`; clicks write the
63    /// toggled value back through the binding, then run any `on_change`.
64    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}