use crate::Theme;
use egui::{Response, Sense, Ui, Vec2};
use egui_cha::ViewCtx;
use std::f32::consts::PI;
use std::ops::RangeInclusive;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArcSliderSize {
Small,
#[default]
Medium,
Large,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArcStyle {
#[default]
Standard,
Full,
Half,
Quarter,
}
pub struct ArcSlider<'a> {
range: RangeInclusive<f64>,
label: Option<&'a str>,
size: ArcSliderSize,
style: ArcStyle,
show_value: bool,
value_suffix: Option<&'a str>,
disabled: bool,
thickness: f32,
}
impl<'a> ArcSlider<'a> {
pub fn new(range: RangeInclusive<f64>) -> Self {
Self {
range,
label: None,
size: ArcSliderSize::default(),
style: ArcStyle::default(),
show_value: true,
value_suffix: None,
disabled: false,
thickness: 1.0,
}
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
pub fn size(mut self, size: ArcSliderSize) -> Self {
self.size = size;
self
}
pub fn small(mut self) -> Self {
self.size = ArcSliderSize::Small;
self
}
pub fn large(mut self) -> Self {
self.size = ArcSliderSize::Large;
self
}
pub fn style(mut self, style: ArcStyle) -> Self {
self.style = style;
self
}
pub fn show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
pub fn suffix(mut self, suffix: &'a str) -> Self {
self.value_suffix = Some(suffix);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn thickness(mut self, thickness: f32) -> Self {
self.thickness = thickness;
self
}
fn get_arc_angles(&self) -> (f32, f32) {
match self.style {
ArcStyle::Standard => (0.75 * PI, 2.25 * PI), ArcStyle::Full => (0.0, 2.0 * PI), ArcStyle::Half => (PI, 2.0 * PI), ArcStyle::Quarter => (1.25 * PI, 1.75 * PI), }
}
pub fn show_with<Msg>(
self,
ctx: &mut ViewCtx<'_, Msg>,
value: f64,
on_change: impl FnOnce(f64) -> Msg,
) {
let mut current = value;
let response = self.show_internal(ctx.ui, &mut current);
if response.changed() {
ctx.emit(on_change(current));
}
}
pub fn show(self, ui: &mut Ui, value: &mut f64) -> Response {
self.show_internal(ui, value)
}
fn show_internal(self, ui: &mut Ui, value: &mut f64) -> Response {
let theme = Theme::current(ui.ctx());
let diameter = match self.size {
ArcSliderSize::Small => theme.spacing_xl + theme.spacing_md, ArcSliderSize::Medium => theme.spacing_xl * 2.0, ArcSliderSize::Large => theme.spacing_xl * 2.0 + theme.spacing_lg, };
let label_height = if self.label.is_some() {
theme.font_size_xs + theme.spacing_xs
} else {
0.0
};
let total_height = diameter + label_height;
let (rect, mut response) = ui.allocate_exact_size(
Vec2::new(diameter, total_height),
if self.disabled {
Sense::hover()
} else {
Sense::click_and_drag()
},
);
if response.dragged() && !self.disabled {
let delta = response.drag_delta();
let sensitivity = 0.003 * (self.range.end() - self.range.start());
let change = (-delta.y + delta.x * 0.5) as f64 * sensitivity;
*value = (*value + change).clamp(*self.range.start(), *self.range.end());
response.mark_changed();
}
if response.double_clicked() && !self.disabled {
*value = (*self.range.start() + *self.range.end()) / 2.0;
response.mark_changed();
}
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let center = rect.center_top() + Vec2::new(0.0, diameter / 2.0);
let radius = diameter / 2.0 - theme.spacing_xs;
let (track_color, arc_color, text_color) = if self.disabled {
(theme.border, theme.text_muted, theme.text_muted)
} else if response.hovered() || response.dragged() {
(theme.border, theme.primary_hover, theme.text_primary)
} else {
(theme.border, theme.primary, theme.text_primary)
};
let (arc_start, arc_end) = self.get_arc_angles();
let stroke_width = theme.stroke_width * 4.0 * self.thickness;
self.draw_arc(
painter,
center,
radius - stroke_width / 2.0,
arc_start,
arc_end,
egui::Stroke::new(stroke_width, track_color),
);
let normalized =
(*value - *self.range.start()) / (*self.range.end() - *self.range.start());
let value_angle = arc_start + (arc_end - arc_start) * normalized as f32;
if normalized > 0.001 {
self.draw_arc(
painter,
center,
radius - stroke_width / 2.0,
arc_start,
value_angle,
egui::Stroke::new(stroke_width, arc_color),
);
}
let cap_radius = stroke_width / 2.0 + 1.0;
let cap_pos = center
+ Vec2::new(
value_angle.sin() * (radius - stroke_width / 2.0),
-value_angle.cos() * (radius - stroke_width / 2.0),
);
painter.circle_filled(cap_pos, cap_radius, arc_color);
if self.show_value {
let value_text = if *self.range.end() - *self.range.start() > 10.0 {
format!("{:.0}", value)
} else {
format!("{:.2}", value)
};
let display_text = if let Some(suffix) = self.value_suffix {
format!("{}{}", value_text, suffix)
} else {
value_text
};
painter.text(
center,
egui::Align2::CENTER_CENTER,
&display_text,
egui::FontId::proportional(theme.font_size_sm),
text_color,
);
}
if let Some(label) = self.label {
let label_pos = rect.center_bottom() - Vec2::new(0.0, theme.spacing_xs);
painter.text(
label_pos,
egui::Align2::CENTER_BOTTOM,
label,
egui::FontId::proportional(theme.font_size_xs),
theme.text_secondary,
);
}
}
response
}
fn draw_arc(
&self,
painter: &egui::Painter,
center: egui::Pos2,
radius: f32,
start_angle: f32,
end_angle: f32,
stroke: egui::Stroke,
) {
let segments = 32;
let angle_step = (end_angle - start_angle) / segments as f32;
let points: Vec<egui::Pos2> = (0..=segments)
.map(|i| {
let angle = start_angle + angle_step * i as f32;
center + Vec2::new(angle.sin() * radius, -angle.cos() * radius)
})
.collect();
painter.add(egui::Shape::line(points, stroke));
}
}