use crate::ext::HasDesignTokens;
use crate::ext::UiExt;
use crate::icons::icons;
use crate::styles::{icons as icon_sizes, typography};
use crate::tokens::DESIGN_TOKENS;
use crate::ui::model::{
PlaybackSpeed, ReplayCommand, ReplayProgress, ToolbarReplayState as ReplayState,
};
use egui::{Response, Ui, Vec2, Widget};
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct ReplayToolbarConfig {
pub show_speed_selector: bool,
pub show_progress: bool,
pub show_time: bool,
pub compact: bool,
}
impl Default for ReplayToolbarConfig {
fn default() -> Self {
Self {
show_speed_selector: true,
show_progress: true,
show_time: true,
compact: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReplayToolbarState {
pub replay_state: ReplayState,
pub speed: PlaybackSpeed,
pub progress: Option<ReplayProgress>,
}
impl ReplayToolbarState {
pub fn sync_from(&mut self, active: bool, playing: bool, speed_multiplier: f32) {
if active {
if playing {
self.replay_state = ReplayState::Playing;
} else {
self.replay_state = ReplayState::Paused;
}
} else {
self.replay_state = ReplayState::Stopped;
}
self.speed = match speed_multiplier {
s if s <= 1.0 => PlaybackSpeed::RealTime,
s if s <= 4.0 => PlaybackSpeed::Fast,
s if s <= 100.0 => PlaybackSpeed::VeryFast,
_ => PlaybackSpeed::UltraFast,
};
}
}
#[derive(Debug, Clone)]
pub enum ReplayToolbarAction {
Play,
Pause,
Stop,
SetSpeed(PlaybackSpeed),
StepForward,
StepBackward,
OpenSettings,
}
pub struct ReplayToolbar<'a> {
state: &'a ReplayToolbarState,
config: ReplayToolbarConfig,
command_sender: Option<&'a Sender<ReplayCommand>>,
}
impl<'a> ReplayToolbar<'a> {
pub fn new(state: &'a ReplayToolbarState) -> Self {
Self {
state,
config: ReplayToolbarConfig::default(),
command_sender: None,
}
}
pub fn with_config(mut self, config: ReplayToolbarConfig) -> Self {
self.config = config;
self
}
pub fn with_command_sender(mut self, sender: &'a Sender<ReplayCommand>) -> Self {
self.command_sender = Some(sender);
self
}
pub fn compact(mut self) -> Self {
self.config.compact = true;
self
}
pub fn show(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
let action = ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = DESIGN_TOKENS.spacing.sm;
let mut action = None;
action = self.show_play_pause_button(ui).or(action);
action = self.show_stop_button(ui).or(action);
ui.space_sm();
action = self.show_step_buttons(ui).or(action);
ui.space_md();
if self.config.show_speed_selector {
action = self.show_speed_selector(ui).or(action);
}
ui.space_md();
if self.config.show_progress {
self.show_progress_bar(ui);
}
if self.config.show_time {
self.show_time_display(ui);
}
if ui
.add(icons::SETTINGS.as_button(Vec2::splat(icon_sizes::COMPACT)))
.clicked()
{
action = Some(ReplayToolbarAction::OpenSettings);
}
action
})
.inner;
if let (Some(sender), Some(act)) = (&self.command_sender, &action) {
let cmd = match act {
ReplayToolbarAction::Play => Some(ReplayCommand::Resume),
ReplayToolbarAction::Pause => Some(ReplayCommand::Pause),
ReplayToolbarAction::Stop => Some(ReplayCommand::Stop),
ReplayToolbarAction::SetSpeed(s) => Some(ReplayCommand::SetSpeed(*s)),
ReplayToolbarAction::StepForward => Some(ReplayCommand::StepForward),
ReplayToolbarAction::StepBackward => Some(ReplayCommand::StepBackward),
ReplayToolbarAction::OpenSettings => None,
};
if let Some(cmd) = cmd {
let _ = sender.send(cmd);
}
}
action
}
fn show_play_pause_button(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
let (icon, tooltip, action) = match self.state.replay_state {
ReplayState::Playing => (&icons::MEDIA_PAUSE, "Pause", ReplayToolbarAction::Pause),
ReplayState::Paused | ReplayState::Stopped | ReplayState::Finished => {
(&icons::MEDIA_PLAY, "Play", ReplayToolbarAction::Play)
}
ReplayState::Buffering => (
&icons::UI_BUFFERING,
"Buffering...",
ReplayToolbarAction::Pause,
),
ReplayState::Error => (&icons::STATUS_ERROR, "Error", ReplayToolbarAction::Stop),
};
let icon_size = if self.config.compact {
icon_sizes::COMPACT
} else {
icon_sizes::SM
};
let response = ui.add(icon.as_button(Vec2::splat(icon_size)));
if response.on_hover_text(tooltip).clicked() {
Some(action)
} else {
None
}
}
fn show_stop_button(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
let enabled = self.state.replay_state.is_active();
let response = ui.add_enabled(
enabled,
icons::MEDIA_STOP.as_button(Vec2::splat(icon_sizes::COMPACT)),
);
if response.on_hover_text("Stop").clicked() {
Some(ReplayToolbarAction::Stop)
} else {
None
}
}
fn show_step_buttons(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
let can_step =
self.state.replay_state == ReplayState::Paused || self.state.replay_state.can_start();
let mut action = None;
if ui
.add_enabled(
can_step,
icons::MEDIA_SKIP_BACK.as_button(Vec2::splat(icon_sizes::COMPACT)),
)
.on_hover_text("Step backward")
.clicked()
{
action = Some(ReplayToolbarAction::StepBackward);
}
if ui
.add_enabled(
can_step,
icons::MEDIA_SKIP_FORWARD.as_button(Vec2::splat(icon_sizes::COMPACT)),
)
.on_hover_text("Step forward")
.clicked()
{
action = Some(ReplayToolbarAction::StepForward);
}
action
}
fn show_speed_selector(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
let mut action = None;
let current = self.state.speed;
egui::ComboBox::from_id_salt("replay_speed")
.selected_text(current.display_name())
.width(DESIGN_TOKENS.sizing.replay.speed_dropdown_width)
.show_ui(ui, |ui| {
for speed in [
PlaybackSpeed::RealTime,
PlaybackSpeed::Fast,
PlaybackSpeed::VeryFast,
PlaybackSpeed::UltraFast,
] {
if ui
.selectable_value(
&mut self.state.speed.clone(),
speed,
speed.display_name(),
)
.clicked()
&& speed != current
{
action = Some(ReplayToolbarAction::SetSpeed(speed));
}
}
});
action
}
fn show_progress_bar(&self, ui: &mut Ui) {
if let Some(ref progress) = self.state.progress {
let pct = progress.progress_pct() as f32;
let desired_size = Vec2::new(
DESIGN_TOKENS.sizing.replay.progress_bar_width / 2.0,
typography::XXL,
);
let (rect, _response) = ui.allocate_exact_size(desired_size, egui::Sense::hover());
if ui.is_rect_visible(rect) {
let painter = ui.painter();
let bg = ui.chart_bg();
let fg = ui.accent_color();
painter.rect_filled(rect, DESIGN_TOKENS.rounding.xs, bg);
let fill_width = rect.width() * pct;
let fill_rect =
egui::Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height()));
painter.rect_filled(fill_rect, DESIGN_TOKENS.rounding.xs, fg);
let text = format!("{:.0}%", pct * 100.0);
painter.text(
rect.center(),
egui::Align2::CENTER_CENTER,
text,
egui::FontId::proportional(typography::XS),
ui.text_color(),
);
}
}
}
fn show_time_display(&self, ui: &mut Ui) {
if let Some(ref progress) = self.state.progress {
let time_str = progress.current_time.format("%H:%M:%S").to_string();
ui.label(egui::RichText::new(time_str).monospace().small());
let bars_str = format!("{}/{}", progress.bars_played, progress.total_bars);
ui.hint_label(bars_str);
}
}
}
impl Widget for ReplayToolbar<'_> {
fn ui(self, ui: &mut Ui) -> Response {
ui.horizontal(|ui| {
self.show(ui);
})
.response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_toolbar_state_default() {
let state = ReplayToolbarState::default();
assert_eq!(state.replay_state, ReplayState::Stopped);
}
#[test]
fn test_toolbar_config_default() {
let config = ReplayToolbarConfig::default();
assert!(config.show_speed_selector);
assert!(config.show_progress);
}
}