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}