use crate::Theme;
use egui::{Color32, Rect, Sense, Stroke, Ui, Vec2};
use egui_cha::ViewCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CrossfaderCurve {
#[default]
Linear,
EqualPower,
FastCut,
Smooth,
}
impl CrossfaderCurve {
pub fn apply(&self, value: f32) -> (f32, f32) {
let normalized = (value + 1.0) / 2.0;
match self {
CrossfaderCurve::Linear => (1.0 - normalized, normalized),
CrossfaderCurve::EqualPower => {
let angle = normalized * std::f32::consts::FRAC_PI_2;
(angle.cos(), angle.sin())
}
CrossfaderCurve::FastCut => {
let a = if normalized < 0.1 {
1.0
} else if normalized > 0.9 {
0.0
} else {
1.0 - (normalized - 0.1) / 0.8
};
let b = if normalized > 0.9 {
1.0
} else if normalized < 0.1 {
0.0
} else {
(normalized - 0.1) / 0.8
};
(a, b)
}
CrossfaderCurve::Smooth => {
let t = normalized;
let smooth = t * t * (3.0 - 2.0 * t);
(1.0 - smooth, smooth)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CrossfaderOrientation {
#[default]
Horizontal,
Vertical,
}
pub struct CrossFader<'a> {
value: f32,
label_a: Option<&'a str>,
label_b: Option<&'a str>,
curve: CrossfaderCurve,
orientation: CrossfaderOrientation,
width: f32,
height: f32,
show_levels: bool,
show_center_detent: bool,
color_a: Option<Color32>,
color_b: Option<Color32>,
}
impl<'a> CrossFader<'a> {
pub fn new() -> Self {
Self {
value: 0.0,
label_a: None,
label_b: None,
curve: CrossfaderCurve::default(),
orientation: CrossfaderOrientation::default(),
width: 200.0,
height: 40.0,
show_levels: true,
show_center_detent: true,
color_a: None,
color_b: None,
}
}
pub fn value(mut self, value: f32) -> Self {
self.value = value.clamp(-1.0, 1.0);
self
}
pub fn labels(mut self, a: &'a str, b: &'a str) -> Self {
self.label_a = Some(a);
self.label_b = Some(b);
self
}
pub fn curve(mut self, curve: CrossfaderCurve) -> Self {
self.curve = curve;
self
}
pub fn orientation(mut self, orientation: CrossfaderOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self
}
pub fn show_levels(mut self, show: bool) -> Self {
self.show_levels = show;
self
}
pub fn show_center_detent(mut self, show: bool) -> Self {
self.show_center_detent = show;
self
}
pub fn color_a(mut self, color: Color32) -> Self {
self.color_a = Some(color);
self
}
pub fn color_b(mut self, color: Color32) -> Self {
self.color_b = Some(color);
self
}
pub fn show_with<Msg>(self, ctx: &mut ViewCtx<'_, Msg>, on_change: impl FnOnce(f32) -> Msg) {
if let Some(new_value) = self.render(ctx.ui) {
ctx.emit(on_change(new_value));
}
}
pub fn show(self, ui: &mut Ui) -> Option<f32> {
self.render(ui)
}
fn render(self, ui: &mut Ui) -> Option<f32> {
let theme = Theme::current(ui.ctx());
let mut new_value = None;
let is_horizontal = self.orientation == CrossfaderOrientation::Horizontal;
let (total_width, total_height) = if is_horizontal {
(self.width, self.height)
} else {
(self.height, self.width)
};
let label_height = if self.label_a.is_some() || self.label_b.is_some() {
theme.font_size_xs + theme.spacing_xs
} else {
0.0
};
let level_height = if self.show_levels { 6.0 } else { 0.0 };
let full_height = total_height + label_height + level_height;
let (rect, response) =
ui.allocate_exact_size(Vec2::new(total_width, full_height), Sense::click_and_drag());
if !ui.is_rect_visible(rect) {
return None;
}
if response.dragged() {
let delta = if is_horizontal {
response.drag_delta().x
} else {
-response.drag_delta().y
};
let range = if is_horizontal {
total_width
} else {
total_height
};
let delta_normalized = delta / (range * 0.5);
let new_val = (self.value + delta_normalized).clamp(-1.0, 1.0);
new_value = Some(new_val);
}
if response.clicked() {
if let Some(pos) = response.interact_pointer_pos() {
let track_rect = Rect::from_min_size(
rect.min + Vec2::new(0.0, label_height),
Vec2::new(total_width, total_height - level_height),
);
let normalized = if is_horizontal {
(pos.x - track_rect.min.x) / track_rect.width()
} else {
1.0 - (pos.y - track_rect.min.y) / track_rect.height()
};
let new_val = (normalized * 2.0 - 1.0).clamp(-1.0, 1.0);
new_value = Some(new_val);
}
}
let current_value = new_value.unwrap_or(self.value);
let (a_level, b_level) = self.curve.apply(current_value);
let painter = ui.painter();
let color_a = self.color_a.unwrap_or(theme.primary);
let color_b = self.color_b.unwrap_or(theme.state_warning);
let track_rect = Rect::from_min_size(
rect.min + Vec2::new(0.0, label_height),
Vec2::new(total_width, total_height - level_height - label_height),
);
if let Some(label_a) = self.label_a {
painter.text(
egui::pos2(rect.min.x + 4.0, rect.min.y + label_height / 2.0),
egui::Align2::LEFT_CENTER,
label_a,
egui::FontId::proportional(theme.font_size_xs),
color_a,
);
}
if let Some(label_b) = self.label_b {
painter.text(
egui::pos2(rect.max.x - 4.0, rect.min.y + label_height / 2.0),
egui::Align2::RIGHT_CENTER,
label_b,
egui::FontId::proportional(theme.font_size_xs),
color_b,
);
}
painter.rect_filled(track_rect, theme.radius_sm, theme.bg_secondary);
let center_x = track_rect.center().x;
let thumb_pos = (current_value + 1.0) / 2.0;
let thumb_x = track_rect.min.x + thumb_pos * track_rect.width();
let a_rect = Rect::from_min_max(track_rect.min, egui::pos2(center_x, track_rect.max.y));
let a_fill = Color32::from_rgba_unmultiplied(
color_a.r(),
color_a.g(),
color_a.b(),
(a_level * 100.0) as u8,
);
painter.rect_filled(a_rect, theme.radius_sm, a_fill);
let b_rect = Rect::from_min_max(egui::pos2(center_x, track_rect.min.y), track_rect.max);
let b_fill = Color32::from_rgba_unmultiplied(
color_b.r(),
color_b.g(),
color_b.b(),
(b_level * 100.0) as u8,
);
painter.rect_filled(b_rect, theme.radius_sm, b_fill);
if self.show_center_detent {
painter.line_segment(
[
egui::pos2(center_x, track_rect.min.y),
egui::pos2(center_x, track_rect.max.y),
],
Stroke::new(1.0, theme.text_muted),
);
}
let thumb_width = 16.0;
let thumb_rect = Rect::from_center_size(
egui::pos2(thumb_x, track_rect.center().y),
Vec2::new(thumb_width, track_rect.height() - 4.0),
);
let thumb_color = if response.hovered() || response.dragged() {
theme.text_primary
} else {
theme.text_secondary
};
painter.rect_filled(thumb_rect, theme.radius_sm * 0.5, thumb_color);
for i in -1..=1 {
let line_x = thumb_rect.center().x + i as f32 * 3.0;
painter.line_segment(
[
egui::pos2(line_x, thumb_rect.min.y + 4.0),
egui::pos2(line_x, thumb_rect.max.y - 4.0),
],
Stroke::new(1.0, theme.bg_primary),
);
}
if self.show_levels {
let level_y = track_rect.max.y + 2.0;
let level_rect_height = level_height - 2.0;
let a_level_width = (center_x - track_rect.min.x) * a_level;
let a_level_rect = Rect::from_min_size(
egui::pos2(track_rect.min.x, level_y),
Vec2::new(a_level_width, level_rect_height),
);
painter.rect_filled(a_level_rect, 2.0, color_a);
let b_level_width = (track_rect.max.x - center_x) * b_level;
let b_level_rect = Rect::from_min_size(
egui::pos2(track_rect.max.x - b_level_width, level_y),
Vec2::new(b_level_width, level_rect_height),
);
painter.rect_filled(b_level_rect, 2.0, color_b);
}
painter.rect_stroke(
track_rect,
theme.radius_sm,
Stroke::new(theme.border_width, theme.border),
egui::StrokeKind::Inside,
);
new_value
}
}
impl Default for CrossFader<'_> {
fn default() -> Self {
Self::new()
}
}