use super::{ReplayAction, ReplayConfig, ReplaySpeed, ReplayState};
use crate::ext::UiExt;
use crate::styles::stroke;
use crate::tokens::DESIGN_TOKENS;
use egui::{RichText, Ui, Vec2};
pub struct ReplayControls<'a> {
config: &'a ReplayConfig,
}
impl<'a> ReplayControls<'a> {
pub fn new(config: &'a ReplayConfig) -> Self {
Self { config }
}
pub fn show(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
let _bars_advanced = state.update();
let layout = &self.config.layout;
let mut result_action = ReplayAction::None;
ui.horizontal(|ui| {
ui.set_height(layout.control_bar_height);
ui.add_space(layout.padding);
if let action @ ReplayAction::ToggleReplayMode = self.render_mode_toggle(ui, state) {
result_action = action;
}
ui.add_space(layout.btn_spacing);
ui.separator();
ui.add_space(layout.btn_spacing);
if state.is_active() {
let playback_action = self.render_playback_controls(ui, state);
if !matches!(playback_action, ReplayAction::None) {
result_action = playback_action;
}
ui.add_space(layout.btn_spacing);
ui.separator();
ui.add_space(layout.btn_spacing);
let progress_action = self.render_progress_bar(ui, state);
if !matches!(progress_action, ReplayAction::None) {
result_action = progress_action;
}
ui.add_space(layout.btn_spacing);
ui.separator();
ui.add_space(layout.btn_spacing);
let speed_action = self.render_speed_control(ui, state);
if !matches!(speed_action, ReplayAction::None) {
result_action = speed_action;
}
ui.add_space(layout.btn_spacing);
ui.separator();
ui.add_space(layout.btn_spacing);
self.render_info_display(ui, state);
} else {
ui.label(
RichText::new("Click 'Replay' to enter replay mode")
.color(self.config.colors.disabled_color),
);
}
ui.add_space(layout.padding);
});
result_action
}
fn render_mode_toggle(&self, ui: &mut Ui, state: &ReplayState) -> ReplayAction {
let is_active = state.is_active();
let btn_text = if is_active { "Exit" } else { "Replay" };
let btn_color = if is_active {
self.config.colors.stop_color
} else {
self.config.colors.active_indicator
};
let button =
egui::Button::new(RichText::new(btn_text).color(btn_color)).min_size(Vec2::new(
DESIGN_TOKENS.sizing.replay.speed_dropdown_width,
self.config.layout.btn_size,
));
if ui.add(button).clicked() {
ReplayAction::ToggleReplayMode
} else {
ReplayAction::None
}
}
fn render_playback_controls(&self, ui: &mut Ui, state: &ReplayState) -> ReplayAction {
let layout = &self.config.layout;
let colors = &self.config.colors;
let mut action = ReplayAction::None;
let step_back_enabled = !state.at_start();
let step_back_btn = egui::Button::new("|<").min_size(Vec2::splat(layout.small_button_size));
let step_back_res = ui.add_enabled(step_back_enabled, step_back_btn);
if step_back_res.clicked() {
action = ReplayAction::StepBackward;
}
step_back_res.on_hover_text("Step backward (Left)");
ui.add_space(layout.btn_spacing);
let (play_pause_icon, play_pause_color, play_pause_hint) = if state.is_playing() {
("||", colors.pause_color, "Pause (Space)")
} else {
(">", colors.play_color, "Play (Space)")
};
let play_pause_btn =
egui::Button::new(RichText::new(play_pause_icon).color(play_pause_color))
.min_size(Vec2::splat(layout.btn_size));
if ui
.add(play_pause_btn)
.on_hover_text(play_pause_hint)
.clicked()
{
action = ReplayAction::TogglePlayPause;
}
ui.add_space(layout.btn_spacing);
let step_fwd_enabled = !state.at_end();
let step_fwd_btn = egui::Button::new(">|").min_size(Vec2::splat(layout.small_button_size));
let step_fwd_res = ui.add_enabled(step_fwd_enabled, step_fwd_btn);
if step_fwd_res.clicked() {
action = ReplayAction::StepForward;
}
step_fwd_res.on_hover_text("Step forward (Right)");
ui.add_space(layout.btn_spacing);
let stop_btn = egui::Button::new(RichText::new("[]").color(colors.stop_color))
.min_size(Vec2::splat(layout.small_button_size));
if ui.add(stop_btn).on_hover_text("Reset to start").clicked() {
action = ReplayAction::Reset;
}
action
}
fn render_progress_bar(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
let layout = &self.config.layout;
let colors = &self.config.colors;
let mut action = ReplayAction::None;
let progress = state.progress();
let width = layout
.progress_bar_width
.unwrap_or(DESIGN_TOKENS.sizing.replay.progress_bar_width);
let (rect, response) = ui.allocate_exact_size(
Vec2::new(width, layout.progress_bar_height),
egui::Sense::click_and_drag(),
);
if ui.is_rect_visible(rect) {
ui.painter()
.rect_filled(rect, DESIGN_TOKENS.rounding.xs, colors.progress_bg);
let filled_width = rect.width() * progress;
let filled_rect =
egui::Rect::from_min_size(rect.min, Vec2::new(filled_width, rect.height()));
ui.painter()
.rect_filled(filled_rect, DESIGN_TOKENS.rounding.xs, colors.progress_fill);
let cursor_x = rect.min.x + filled_width;
let cursor_center = egui::pos2(cursor_x, rect.center().y);
ui.painter().circle_filled(
cursor_center,
layout.progress_bar_height * 0.8,
colors.progress_cursor,
);
for marker in &state.markers {
if state.total_bars > 1 {
let marker_progress = marker.bar_idx as f32 / (state.total_bars - 1) as f32;
let marker_x = rect.min.x + rect.width() * marker_progress;
let marker_color = egui::Color32::from_rgba_unmultiplied(
marker.color[0],
marker.color[1],
marker.color[2],
marker.color[3],
);
ui.painter().line_segment(
[
egui::pos2(marker_x, rect.min.y),
egui::pos2(marker_x, rect.max.y),
],
egui::Stroke::new(stroke::MEDIUM, marker_color),
);
}
}
}
if (response.clicked() || response.dragged())
&& let Some(pos) = response.interact_pointer_pos()
{
let relative_x = (pos.x - rect.min.x) / rect.width();
let new_progress = relative_x.clamp(0.0, 1.0);
action = ReplayAction::JumpToPercent(new_progress);
}
response.on_hover_text(format!(
"Bar {} / {} ({:.1}%)",
state.curr_bar + 1,
state.total_bars,
progress * 100.0
));
action
}
fn render_speed_control(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
let layout = &self.config.layout;
let colors = &self.config.colors;
let mut action = ReplayAction::None;
let slow_btn = egui::Button::new("-").min_size(Vec2::splat(layout.small_button_size));
if ui.add(slow_btn).on_hover_text("Slow down").clicked() {
action = ReplayAction::SlowDown;
}
let speed_label = state.speed.label();
let combo_id = ui.id().with("speed_combo");
egui::ComboBox::from_id_salt(combo_id)
.selected_text(RichText::new(&speed_label).color(colors.speed_color))
.width(layout.speed_dropdown_width)
.show_ui(ui, |ui| {
for preset in ReplaySpeed::presets() {
let label = preset.label();
if ui
.selectable_label(*preset == state.speed, &label)
.clicked()
{
action = ReplayAction::SetSpeed(preset.multiplier());
}
}
});
let fast_btn = egui::Button::new("+").min_size(Vec2::splat(layout.small_button_size));
if ui.add(fast_btn).on_hover_text("Speed up").clicked() {
action = ReplayAction::SpeedUp;
}
action
}
fn render_info_display(&self, ui: &mut Ui, state: &ReplayState) {
let behavior = &self.config.behavior;
let layout = &self.config.layout;
ui.right_aligned(|ui| {
if behavior.show_bar_counter {
ui.label(
RichText::new(format!("{}/{}", state.curr_bar + 1, state.total_bars)).small(),
);
}
if let Some(date) = state.curr_date {
ui.add_space(layout.btn_spacing);
ui.label(RichText::new(date.format("%Y-%m-%d %H:%M").to_string()).small());
}
if let Some(ref symbol) = state.symbol {
ui.add_space(layout.btn_spacing);
ui.strong_label(symbol);
}
});
}
}
pub struct CompactReplayControls<'a> {
config: &'a ReplayConfig,
}
impl<'a> CompactReplayControls<'a> {
pub fn new(config: &'a ReplayConfig) -> Self {
Self { config }
}
pub fn show(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
let mut action = ReplayAction::None;
let colors = &self.config.colors;
let _bars = state.update();
ui.horizontal(|ui| {
let icon = if state.is_playing() { "||" } else { ">" };
if ui.small_button(icon).clicked() {
action = ReplayAction::TogglePlayPause;
}
let progress = state.progress();
let progress_bar = egui::ProgressBar::new(progress)
.show_percentage()
.fill(colors.progress_fill);
let response = ui.add(progress_bar);
if response.clicked()
&& let Some(pos) = response.interact_pointer_pos()
{
let rect = response.rect;
let relative_x = (pos.x - rect.min.x) / rect.width();
action = ReplayAction::JumpToPercent(relative_x.clamp(0.0, 1.0));
}
ui.label(
RichText::new(state.speed.label())
.small()
.color(colors.speed_color),
);
});
action
}
}
pub struct ReplayKeyboardHandler;
impl ReplayKeyboardHandler {
pub fn handle(ui: &Ui, state: &ReplayState) -> ReplayAction {
if !state.is_active() {
return ReplayAction::None;
}
if ui.ctx().memory(|mem| mem.focused().is_some()) {
return ReplayAction::None;
}
ui.input(|i| {
if i.key_pressed(egui::Key::Space) {
return ReplayAction::TogglePlayPause;
}
if i.key_pressed(egui::Key::ArrowLeft) {
if i.modifiers.shift {
return ReplayAction::StepBackwardN(10);
}
return ReplayAction::StepBackward;
}
if i.key_pressed(egui::Key::ArrowRight) {
if i.modifiers.shift {
return ReplayAction::StepForwardN(10);
}
return ReplayAction::StepForward;
}
if i.key_pressed(egui::Key::ArrowUp) {
return ReplayAction::SpeedUp;
}
if i.key_pressed(egui::Key::ArrowDown) {
return ReplayAction::SlowDown;
}
if i.key_pressed(egui::Key::Home) {
return ReplayAction::JumpToStart;
}
if i.key_pressed(egui::Key::End) {
return ReplayAction::JumpToEnd;
}
if i.key_pressed(egui::Key::M) {
return ReplayAction::AddMarker {
label: None,
color: None,
};
}
if i.key_pressed(egui::Key::R) && i.modifiers.ctrl {
return ReplayAction::Reset;
}
if i.key_pressed(egui::Key::Escape) {
return ReplayAction::ExitReplayMode;
}
ReplayAction::None
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_controls_creation() {
let config = ReplayConfig::default();
let _controls = ReplayControls::new(&config);
}
#[test]
fn test_compact_controls_creation() {
let config = ReplayConfig::compact();
let _controls = CompactReplayControls::new(&config);
}
}