egui_macview/controls/
toggle.rs1use crate::theme::MacTheme;
6use egui::{CornerRadius, Pos2, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget};
7
8pub struct MacToggle<'a> {
28 value: &'a mut bool,
29 enabled: bool,
30 theme: Option<&'a MacTheme>,
31}
32
33impl<'a> MacToggle<'a> {
34 pub fn new(value: &'a mut bool) -> Self {
36 Self {
37 value,
38 enabled: true,
39 theme: None,
40 }
41 }
42
43 pub fn enabled(mut self, enabled: bool) -> Self {
45 self.enabled = enabled;
46 self
47 }
48
49 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
51 self.theme = Some(theme);
52 self
53 }
54}
55
56impl<'a> Widget for MacToggle<'a> {
57 fn ui(self, ui: &mut Ui) -> Response {
58 let fallback_theme;
59 let theme = match self.theme {
60 Some(t) => t,
61 None => {
62 fallback_theme = MacTheme::from_ctx(ui.ctx());
63 &fallback_theme
64 }
65 };
66
67 let metrics = theme.toggle();
68 let height = metrics.height.points();
69 let width = metrics.width.points();
70 let radius = metrics.corner_radius.points() as u8;
71
72 let desired_size = Vec2::new(width, height);
73 let sense = if self.enabled {
74 Sense::click()
75 } else {
76 Sense::hover()
77 };
78 let (rect, mut response) = ui.allocate_exact_size(desired_size, sense);
79 if self.enabled && response.clicked() {
80 *self.value = !*self.value;
81 response.mark_changed();
82 }
83
84 let spring = theme.motion().swiftui_bouncy.to_spring();
86 let now = ui.input(|i| i.time);
87 let spring_key = response.id.with("toggle_caspring");
88 let target_val = if *self.value { 1.0f32 } else { 0.0f32 };
89 let (prev_target, start_val, start_time) = ui.data(|d| {
90 d.get_temp::<(f32, f32, f64)>(spring_key)
91 .unwrap_or((target_val, target_val, now))
92 });
93
94 let anim_progress = if (prev_target - target_val).abs() > f32::EPSILON {
95 let elapsed = (now - start_time).max(0.0) as f32;
96 let cur = start_val + (prev_target - start_val) * spring.position_at(elapsed);
97 ui.data_mut(|d| d.insert_temp(spring_key, (target_val, cur, now)));
98 ui.ctx().request_repaint();
99 cur.clamp(0.0, 1.0)
100 } else {
101 let elapsed = (now - start_time).max(0.0) as f32;
102 if elapsed < spring.settling_duration() {
103 ui.ctx().request_repaint();
104 (start_val + (target_val - start_val) * spring.position_at(elapsed)).clamp(0.0, 1.0)
105 } else {
106 target_val
107 }
108 };
109
110 let alpha_factor = if self.enabled {
111 1.0
112 } else {
113 theme.disabled_opacity()
114 };
115 let painter = ui.painter_at(rect);
116 let bg_color = theme
117 .control()
118 .lerp_to_gamma(theme.system_green(), anim_progress)
119 .gamma_multiply(alpha_factor);
120
121 painter.rect(
122 rect,
123 CornerRadius::same(radius),
124 bg_color,
125 Stroke::new(
126 metrics.border_width.points(),
127 theme.separator().gamma_multiply(alpha_factor),
128 ),
129 StrokeKind::Inside,
130 );
131
132 let knob_radius = metrics.knob_size.points() * 0.5;
133 let x_off = rect.min.x + metrics.knob_offset_off.points() + knob_radius;
134 let x_on = rect.min.x + metrics.knob_offset_on.points() + knob_radius;
135 let knob_x = x_off + (x_on - x_off) * anim_progress;
136 let knob_center = Pos2::new(knob_x, rect.center().y);
137
138 painter.circle_filled(
139 knob_center + Vec2::new(0.0, metrics.border_width.points()),
140 knob_radius,
141 theme.drop_shadow().gamma_multiply(alpha_factor),
142 );
143 painter.circle_filled(
144 knob_center,
145 knob_radius,
146 theme.knob_fill().gamma_multiply(alpha_factor),
147 );
148
149 response
150 }
151}