1use egui::{
19 pos2, vec2, Color32, CornerRadius, FontSelection, Response, Sense, Stroke, Ui, Vec2, Widget,
20 WidgetInfo, WidgetText, WidgetType,
21};
22
23use crate::theme::{mix, with_alpha, Theme};
24use crate::Accent;
25
26#[must_use = "Add this widget with `ui.add(...)`."]
28pub struct Switch<'a> {
29 state: &'a mut bool,
30 label: WidgetText,
31 accent: Accent,
32 enabled: bool,
33}
34
35impl<'a> std::fmt::Debug for Switch<'a> {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("Switch")
38 .field("state", &*self.state)
39 .field("label", &self.label.text())
40 .field("accent", &self.accent)
41 .field("enabled", &self.enabled)
42 .finish()
43 }
44}
45
46impl<'a> Switch<'a> {
47 pub fn new(state: &'a mut bool, label: impl Into<WidgetText>) -> Self {
52 Self {
53 state,
54 label: label.into(),
55 accent: Accent::Sky,
56 enabled: true,
57 }
58 }
59
60 pub fn accent(mut self, accent: Accent) -> Self {
62 self.accent = accent;
63 self
64 }
65
66 pub fn enabled(mut self, enabled: bool) -> Self {
69 self.enabled = enabled;
70 self
71 }
72}
73
74impl<'a> Widget for Switch<'a> {
75 fn ui(self, ui: &mut Ui) -> Response {
76 let theme = Theme::current(ui.ctx());
77 let p = &theme.palette;
78 let t = &theme.typography;
79
80 let track_w: f32 = 32.0;
81 let track_h: f32 = 18.0;
82 let knob_pad: f32 = 2.0;
83 let knob_d: f32 = track_h - knob_pad * 2.0;
84 let gap: f32 = 8.0;
85
86 let label_text = self.label.text();
87 let has_label = !label_text.is_empty();
88
89 let galley = has_label.then(|| {
90 egui::WidgetText::from(egui::RichText::new(label_text).color(p.text).size(t.body))
91 .into_galley(
92 ui,
93 Some(egui::TextWrapMode::Extend),
94 ui.available_width(),
95 FontSelection::FontId(egui::FontId::proportional(t.body)),
96 )
97 });
98
99 let text_size = galley.as_ref().map_or(Vec2::ZERO, |g| g.size());
100 let desired = vec2(
101 track_w + if has_label { gap + text_size.x } else { 0.0 },
102 track_h.max(text_size.y),
103 );
104
105 let sense = if self.enabled {
106 Sense::click()
107 } else {
108 Sense::hover()
109 };
110 let (rect, mut response) = ui.allocate_exact_size(desired, sense);
111
112 if self.enabled && response.clicked() {
113 *self.state = !*self.state;
114 response.mark_changed();
115 }
116
117 if ui.is_rect_visible(rect) {
118 let on = *self.state;
119 let progress = ui.ctx().animate_bool_responsive(response.id, on);
120
121 let track_rect = egui::Rect::from_min_size(
122 pos2(rect.min.x, rect.center().y - track_h * 0.5),
123 vec2(track_w, track_h),
124 );
125
126 let accent = p.accent_fill(self.accent);
127 let hovered = self.enabled && response.hovered();
128
129 let off_fill = p.input_bg;
130 let track_fill = if !self.enabled {
131 with_alpha(off_fill, 160)
132 } else {
133 mix(off_fill, accent, progress)
134 };
135 let stroke_color = if !self.enabled {
136 with_alpha(p.border, 160)
137 } else if progress > 0.05 {
138 mix(p.border, accent, progress)
139 } else if hovered {
140 p.sky
141 } else {
142 p.border
143 };
144
145 ui.painter().rect(
146 track_rect,
147 CornerRadius::same((track_h * 0.5) as u8),
148 track_fill,
149 Stroke::new(1.0, stroke_color),
150 egui::StrokeKind::Inside,
151 );
152
153 let travel = track_w - knob_d - knob_pad * 2.0;
154 let knob_center = pos2(
155 track_rect.min.x + knob_pad + knob_d * 0.5 + travel * progress,
156 track_rect.center().y,
157 );
158 let knob_color = if self.enabled {
159 if p.is_dark {
160 Color32::WHITE
161 } else {
162 mix(p.text_muted, Color32::WHITE, progress)
166 }
167 } else {
168 p.text_muted
169 };
170 ui.painter()
171 .circle_filled(knob_center, knob_d * 0.5, knob_color);
172
173 if let Some(g) = galley {
174 let text_pos = pos2(track_rect.max.x + gap, rect.center().y - text_size.y * 0.5);
175 let color = if self.enabled { p.text } else { p.text_faint };
176 ui.painter().galley(text_pos, g, color);
177 }
178 }
179
180 response.widget_info(|| {
181 WidgetInfo::labeled(WidgetType::Checkbox, self.enabled, self.label.text())
182 });
183 response
184 }
185}