use core::fmt;
use egui::{Align2, Color32, FontId, Stroke, StrokeKind, Ui, Vec2, pos2, vec2};
use crate::colors::{GRAY, SUCCESS, WARN, get_text_color};
#[derive(Clone, Copy, Debug)]
pub enum ToggleStyle {
Button,
Relay,
Valve,
}
pub struct ToggleSwitch<'a> {
on: &'a mut bool,
label: Option<String>,
color: Color32,
style: ToggleStyle,
size: Option<Vec2>,
font_size: f32,
}
impl<'a> ToggleSwitch<'a> {
pub fn new(on: &'a mut bool) -> Self {
Self {
on,
label: None,
color: Color32::WHITE,
style: ToggleStyle::Button,
size: None,
font_size: 14.0,
}
}
pub fn label(mut self, label: impl fmt::Display) -> Self {
self.label = Some(label.to_string());
self
}
pub fn color(mut self, color: Color32) -> Self {
self.color = color;
self
}
pub fn style(mut self, style: ToggleStyle) -> Self {
self.style = style;
self
}
pub fn size(mut self, size: Vec2) -> Self {
self.size = Some(size);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
}
impl egui::Widget for ToggleSwitch<'_> {
#[allow(clippy::too_many_lines)]
fn ui(self, ui: &mut Ui) -> egui::Response {
let has_user_defined_size = self.size.is_some();
let default_toggle_size = match self.style {
ToggleStyle::Button => vec2(2.0, 1.0),
ToggleStyle::Relay => vec2(3.0, 1.5),
ToggleStyle::Valve => vec2(3.0, 3.0),
} * ui.spacing().interact_size.y;
let (label_size, label_spacing) = if let Some(ref label_text) = self.label {
let text_size = ui.fonts_mut(|f| {
let galley = f.layout_no_wrap(
label_text.to_string(),
FontId::proportional(self.font_size),
ui.visuals().text_color(),
);
galley.size()
});
(Some(text_size), ui.spacing().item_spacing.x)
} else {
(None, 0.0)
};
let full_size = if has_user_defined_size {
self.size.unwrap()
} else if let Some(text_size) = label_size {
vec2(
default_toggle_size.x + label_spacing + text_size.x,
default_toggle_size.y.max(text_size.y),
)
} else {
default_toggle_size
};
let (rect, mut response) = ui.allocate_exact_size(full_size, egui::Sense::click());
if response.clicked() {
*self.on = !*self.on;
response.mark_changed();
}
response.widget_info(|| {
egui::WidgetInfo::selected(
egui::WidgetType::Checkbox,
ui.is_enabled(),
*self.on,
self.label.as_ref().map_or("", |s| s.as_str()),
)
});
if ui.is_rect_visible(rect) {
let stroke_color = if *self.on { SUCCESS } else { WARN };
let stroke = Stroke::new(1.0, stroke_color);
let corner_radius = 4.0;
let painter = ui.painter();
if matches!(self.style, ToggleStyle::Relay) {
painter.rect_stroke(rect, corner_radius, stroke, StrokeKind::Inside);
}
let toggle_size = if has_user_defined_size && label_size.is_some() {
let label_text_size = label_size.unwrap();
let available_width = full_size.x - label_spacing - label_text_size.x;
let toggle_width = available_width.max(default_toggle_size.x * 0.6);
vec2(toggle_width, full_size.y)
} else if has_user_defined_size {
full_size
} else {
default_toggle_size
};
let rect_with_margin = egui::Rect::from_min_size(rect.min, toggle_size);
let inner_margin = if matches!(self.style, ToggleStyle::Button) {
0.0
} else {
ui.spacing().item_spacing.x
};
if matches!(self.style, ToggleStyle::Valve) {
painter.rect_stroke(rect_with_margin, corner_radius, stroke, StrokeKind::Inside);
}
let toggle_rect = rect_with_margin.shrink(inner_margin);
let how_on = ui.ctx().animate_bool_responsive(response.id, *self.on);
match self.style {
ToggleStyle::Button => {
let radius = 0.5 * toggle_rect.height();
painter.rect(
toggle_rect,
radius,
if *self.on { SUCCESS } else { GRAY },
Stroke::NONE,
StrokeKind::Inside,
);
let circle_x = egui::lerp(
(toggle_rect.left() + radius)..=(toggle_rect.right() - radius),
how_on,
);
let center = pos2(circle_x, toggle_rect.center().y);
painter.circle(center, 0.75 * radius, Color32::WHITE, Stroke::NONE);
}
ToggleStyle::Relay => {
let center_y = toggle_rect.center().y;
let circle_radius = toggle_rect.width() * 0.04;
let node_left = toggle_rect.left() + toggle_rect.width() / 3.0;
let node_right = toggle_rect.right() - toggle_rect.width() / 3.0;
painter.line_segment(
[
pos2(toggle_rect.left(), center_y),
pos2(node_left - circle_radius * 2.0, center_y),
],
stroke,
);
let circle_left = pos2(node_left - circle_radius, center_y);
let circle_right = pos2(node_right + circle_radius, center_y);
painter.circle_stroke(circle_left, circle_radius, stroke);
painter.circle_stroke(circle_right, circle_radius, stroke);
painter.line_segment(
[
pos2(toggle_rect.right(), center_y),
pos2(node_right + circle_radius * 2.0, center_y),
],
stroke,
);
let switch_left = pos2(node_left, center_y);
let switch_right = pos2(node_right, center_y);
let off_angle = 45.0_f32.to_radians();
let t = ui.ctx().animate_bool(response.id, *self.on);
let angle = egui::lerp(off_angle..=0.0, t);
let length = (switch_right - switch_left).length();
let dir = vec2(angle.cos(), angle.sin());
let animated_left = switch_right - dir * length;
painter.line_segment([animated_left, switch_right], stroke);
}
ToggleStyle::Valve => {
let center = toggle_rect.center();
let radius = toggle_rect.width().min(toggle_rect.height()) * 0.2;
let valve_width = toggle_rect.width().min(toggle_rect.height());
let valve_height = valve_width;
let top_y = center.y - radius * 2.0;
painter.line_segment(
[
pos2(center.x - radius, top_y),
pos2(center.x + radius, top_y),
],
stroke,
);
painter.line_segment(
[pos2(center.x, top_y), pos2(center.x, top_y + radius)],
stroke,
);
painter.add(egui::Shape::line(
vec![
pos2(center.x - valve_width / 2.0, center.y - valve_height / 4.0),
pos2(center.x - valve_width / 2.0, center.y + valve_height / 4.0),
pos2(center.x + valve_width / 2.0, center.y - valve_height / 4.0),
pos2(center.x + valve_width / 2.0, center.y + valve_height / 4.0),
pos2(center.x - valve_width / 2.0, center.y - valve_height / 4.0),
],
stroke,
));
let t = ui.ctx().animate_bool_with_time(response.id, *self.on, 3.0);
if t > 0.0 && t < 1.0 {
painter.circle(center, radius, ui.visuals().panel_fill, Stroke::NONE);
let size = vec2(radius * 2.0, radius * 2.0);
let spinner_rect = egui::Rect::from_center_size(center, size);
let spinner = egui::Spinner::new()
.size(radius * 2.0)
.color(ui.visuals().panel_fill);
spinner.paint_at(ui, spinner_rect);
} else {
painter.circle(center, radius, ui.visuals().panel_fill, stroke);
}
if !*self.on {
painter.line_segment(
[
pos2(center.x - valve_width / 2.0, center.y + valve_height / 4.0),
pos2(center.x + valve_width / 2.0, center.y - valve_height / 4.0),
],
stroke,
);
}
}
}
if let Some(label) = self.label {
let label_pos = if matches!(self.style, ToggleStyle::Valve) {
pos2(rect.right(), rect.center().y)
} else {
pos2(
toggle_rect.right() + ui.spacing().item_spacing.x,
toggle_rect.center().y,
)
};
let anchor = if matches!(self.style, ToggleStyle::Valve) {
Align2::RIGHT_CENTER
} else {
Align2::LEFT_CENTER
};
painter.text(
label_pos,
anchor,
label,
FontId::proportional(self.font_size),
get_text_color(ui),
);
}
}
response
}
}