use crate::Theme;
use egui::{Response, Sense, Ui, Vec2, Widget};
use egui_cha::ViewCtx;
use std::ops::RangeInclusive;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FaderSize {
Compact,
#[default]
Medium,
Large,
}
pub struct Fader<'a> {
range: RangeInclusive<f64>,
label: Option<&'a str>,
size: FaderSize,
show_value: bool,
db_scale: bool,
disabled: bool,
}
impl<'a> Fader<'a> {
pub fn new(range: RangeInclusive<f64>) -> Self {
Self {
range,
label: None,
size: FaderSize::default(),
show_value: true,
db_scale: false,
disabled: false,
}
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
pub fn compact(mut self) -> Self {
self.size = FaderSize::Compact;
self
}
pub fn size(mut self, size: FaderSize) -> Self {
self.size = size;
self
}
pub fn show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
pub fn db_scale(mut self, enabled: bool) -> Self {
self.db_scale = enabled;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
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 (width, height) = match self.size {
FaderSize::Compact => (theme.spacing_lg, theme.spacing_xl * 4.0),
FaderSize::Medium => (theme.spacing_xl, theme.spacing_xl * 5.0),
FaderSize::Large => (theme.spacing_xl + theme.spacing_md, theme.spacing_xl * 6.0),
};
let label_height = if self.label.is_some() {
theme.font_size_xs + theme.spacing_xs
} else {
0.0
};
let value_height = if self.show_value {
theme.font_size_xs + theme.spacing_xs
} else {
0.0
};
let total_height = height + label_height + value_height;
let (rect, mut response) = ui.allocate_exact_size(
Vec2::new(width, total_height),
if self.disabled {
Sense::hover()
} else {
Sense::click_and_drag()
},
);
let track_rect = egui::Rect::from_min_size(
rect.min + Vec2::new(0.0, value_height),
Vec2::new(width, height),
);
if response.dragged() && !self.disabled {
let delta = response.drag_delta();
let range_size = *self.range.end() - *self.range.start();
let sensitivity = range_size / (height - theme.spacing_md) as f64;
*value = (*value - delta.y as f64 * sensitivity)
.clamp(*self.range.start(), *self.range.end());
}
if response.clicked() && !self.disabled {
if let Some(pos) = response.interact_pointer_pos() {
let relative_y = (track_rect.max.y - pos.y) / (height - theme.spacing_md);
let range_size = *self.range.end() - *self.range.start();
*value = (*self.range.start() + relative_y as f64 * range_size)
.clamp(*self.range.start(), *self.range.end());
}
}
if response.double_clicked() && !self.disabled {
if self.db_scale {
*value = 0.0_f64.clamp(*self.range.start(), *self.range.end());
} else {
*value = (*self.range.start() + *self.range.end()) / 2.0;
}
}
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let (track_color, thumb_color, fill_color) = if self.disabled {
(theme.bg_tertiary, theme.text_muted, theme.text_muted)
} else if response.hovered() || response.dragged() {
(theme.bg_tertiary, theme.primary_hover, theme.primary_hover)
} else {
(theme.bg_secondary, theme.primary, theme.primary)
};
let track_inner = track_rect.shrink(theme.spacing_xs);
painter.rect_filled(track_inner, theme.radius_sm, track_color);
painter.rect_stroke(
track_inner,
theme.radius_sm,
egui::Stroke::new(theme.border_width, theme.border),
egui::StrokeKind::Outside,
);
let normalized =
(*value - *self.range.start()) / (*self.range.end() - *self.range.start());
let fill_height = normalized as f32 * (track_inner.height() - theme.spacing_sm);
let fill_rect = egui::Rect::from_min_max(
egui::Pos2::new(
track_inner.min.x + theme.spacing_xs,
track_inner.max.y - theme.spacing_xs / 2.0 - fill_height,
),
egui::Pos2::new(
track_inner.max.x - theme.spacing_xs,
track_inner.max.y - theme.spacing_xs / 2.0,
),
);
if fill_height > 0.0 {
let fill_alpha = if self.disabled { 80 } else { 150 };
let fill_color_alpha = egui::Color32::from_rgba_unmultiplied(
fill_color.r(),
fill_color.g(),
fill_color.b(),
fill_alpha,
);
painter.rect_filled(fill_rect, theme.radius_sm * 0.5, fill_color_alpha);
}
let thumb_y = track_inner.max.y - theme.spacing_xs / 2.0 - fill_height;
let thumb_height = theme.spacing_sm;
let thumb_rect = egui::Rect::from_min_max(
egui::Pos2::new(track_inner.min.x, thumb_y - thumb_height / 2.0),
egui::Pos2::new(track_inner.max.x, thumb_y + thumb_height / 2.0),
);
painter.rect_filled(thumb_rect, theme.radius_sm * 0.5, thumb_color);
let grip_color = if self.disabled {
theme.bg_tertiary
} else {
theme.bg_primary
};
for i in [-1, 0, 1] {
let y = thumb_y + i as f32 * 2.0;
painter.line_segment(
[
egui::Pos2::new(thumb_rect.min.x + theme.spacing_xs, y),
egui::Pos2::new(thumb_rect.max.x - theme.spacing_xs, y),
],
egui::Stroke::new(theme.stroke_width * 0.5, grip_color),
);
}
if self.show_value {
let value_text = if self.db_scale {
if *value <= *self.range.start() + 0.1 {
"-∞".to_string()
} else {
format!("{:.1}dB", value)
}
} else if *self.range.end() - *self.range.start() > 10.0 {
format!("{:.0}", value)
} else {
format!("{:.2}", value)
};
let value_pos = rect.center_top() + Vec2::new(0.0, theme.font_size_xs / 2.0);
painter.text(
value_pos,
egui::Align2::CENTER_CENTER,
&value_text,
egui::FontId::proportional(theme.font_size_xs),
if self.disabled {
theme.text_muted
} else {
theme.text_primary
},
);
}
if let Some(label) = self.label {
let label_pos = rect.center_bottom() - Vec2::new(0.0, theme.font_size_xs / 2.0);
painter.text(
label_pos,
egui::Align2::CENTER_CENTER,
label,
egui::FontId::proportional(theme.font_size_xs),
if self.disabled {
theme.text_muted
} else {
theme.text_secondary
},
);
}
}
if response.dragged() || response.clicked() || response.double_clicked() {
response.mark_changed();
}
response
}
}
impl Widget for Fader<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let mut dummy = 0.5;
self.show_internal(ui, &mut dummy)
}
}