use crate::Theme;
use egui::{CornerRadius, Rect, Sense, Stroke, Ui, Vec2};
use egui_cha::ViewCtx;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TransportEvent {
Play,
Stop,
Record,
BpmChange(f32),
}
pub struct TransportBar {
playing: bool,
recording: bool,
bpm: f32,
show_record: bool,
show_bpm: bool,
compact: bool,
}
impl Default for TransportBar {
fn default() -> Self {
Self::new()
}
}
impl TransportBar {
pub fn new() -> Self {
Self {
playing: false,
recording: false,
bpm: 120.0,
show_record: true,
show_bpm: true,
compact: false,
}
}
pub fn playing(mut self, playing: bool) -> Self {
self.playing = playing;
self
}
pub fn recording(mut self, recording: bool) -> Self {
self.recording = recording;
self
}
pub fn bpm(mut self, bpm: f32) -> Self {
self.bpm = bpm;
self
}
pub fn show_record(mut self, show: bool) -> Self {
self.show_record = show;
self
}
pub fn show_bpm(mut self, show: bool) -> Self {
self.show_bpm = show;
self
}
pub fn compact(mut self, compact: bool) -> Self {
self.compact = compact;
self
}
pub fn show_with<Msg>(
self,
ctx: &mut ViewCtx<'_, Msg>,
on_event: impl Fn(TransportEvent) -> Msg,
) {
if let Some(event) = self.render(ctx.ui) {
ctx.emit(on_event(event));
}
}
pub fn show(self, ui: &mut Ui) -> Option<TransportEvent> {
self.render(ui)
}
fn render(self, ui: &mut Ui) -> Option<TransportEvent> {
let theme = Theme::current(ui.ctx());
let button_size = if self.compact { 28.0 } else { 36.0 };
let spacing = theme.spacing_xs;
let mut event = None;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = spacing;
if self.render_stop_button(ui, button_size, &theme) {
event = Some(TransportEvent::Stop);
}
if self.render_play_button(ui, button_size, &theme) {
event = Some(TransportEvent::Play);
}
if self.show_record {
if self.render_record_button(ui, button_size, &theme) {
event = Some(TransportEvent::Record);
}
}
if self.show_bpm {
ui.add_space(spacing * 2.0);
if let Some(new_bpm) = self.render_bpm(ui, &theme) {
event = Some(TransportEvent::BpmChange(new_bpm));
}
}
});
event
}
fn render_stop_button(&self, ui: &mut Ui, size: f32, theme: &Theme) -> bool {
let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let bg_color = if !self.playing {
theme.primary
} else {
visuals.bg_fill
};
ui.painter()
.rect_filled(rect, CornerRadius::same(4), bg_color);
let icon_size = size * 0.35;
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
let icon_color = if !self.playing {
theme.primary_text
} else {
visuals.fg_stroke.color
};
ui.painter()
.rect_filled(icon_rect, CornerRadius::ZERO, icon_color);
}
response.clicked()
}
fn render_play_button(&self, ui: &mut Ui, size: f32, theme: &Theme) -> bool {
let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let bg_color = if self.playing {
theme.state_success
} else {
visuals.bg_fill
};
ui.painter()
.rect_filled(rect, CornerRadius::same(4), bg_color);
let icon_size = size * 0.4;
let center = rect.center();
let icon_color = if self.playing {
theme.state_success_text
} else {
visuals.fg_stroke.color
};
let points = vec![
egui::pos2(center.x - icon_size * 0.4, center.y - icon_size * 0.5),
egui::pos2(center.x + icon_size * 0.5, center.y),
egui::pos2(center.x - icon_size * 0.4, center.y + icon_size * 0.5),
];
ui.painter().add(egui::Shape::convex_polygon(
points,
icon_color,
Stroke::NONE,
));
}
response.clicked()
}
fn render_record_button(&self, ui: &mut Ui, size: f32, theme: &Theme) -> bool {
let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let bg_color = if self.recording {
theme.state_danger
} else {
visuals.bg_fill
};
ui.painter()
.rect_filled(rect, CornerRadius::same(4), bg_color);
let icon_radius = size * 0.2;
let icon_color = if self.recording {
theme.state_danger_text
} else {
theme.state_danger
};
ui.painter()
.circle_filled(rect.center(), icon_radius, icon_color);
}
response.clicked()
}
fn render_bpm(&self, ui: &mut Ui, theme: &Theme) -> Option<f32> {
let mut new_bpm = None;
ui.horizontal(|ui| {
ui.label(
egui::RichText::new("BPM")
.size(theme.font_size_sm)
.color(theme.text_secondary),
);
let response = ui.add(
egui::DragValue::new(&mut self.bpm.clone())
.range(20.0..=300.0)
.speed(0.5)
.fixed_decimals(1)
.custom_formatter(|v, _| format!("{:.1}", v)),
);
if response.changed() {
if let Some(bpm) = response
.drag_delta()
.y
.abs()
.gt(&0.0)
.then(|| (self.bpm - response.drag_delta().y * 0.5).clamp(20.0, 300.0))
{
new_bpm = Some(bpm);
}
}
});
new_bpm
}
}
pub struct BeatIndicator {
beats: usize,
current: usize,
size: f32,
}
impl BeatIndicator {
pub fn new(beats: usize) -> Self {
Self {
beats,
current: 0,
size: 16.0,
}
}
pub fn current_beat(mut self, beat: usize) -> Self {
self.current = beat;
self
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn show(self, ui: &mut Ui) {
let theme = Theme::current(ui.ctx());
let spacing = theme.spacing_xs * 0.5;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = spacing;
for i in 0..self.beats {
let (rect, _response) =
ui.allocate_exact_size(Vec2::splat(self.size), Sense::hover());
if ui.is_rect_visible(rect) {
let is_current = i == self.current;
let is_downbeat = i == 0;
let color = if is_current {
if is_downbeat {
theme.state_warning } else {
theme.primary
}
} else {
theme.bg_tertiary
};
let radius = if is_current {
self.size * 0.45
} else {
self.size * 0.35
};
ui.painter().circle_filled(rect.center(), radius, color);
if !is_current {
ui.painter().circle_stroke(
rect.center(),
radius,
Stroke::new(1.0, theme.border),
);
}
}
}
});
}
}