Skip to main content

egui_charts/ui/replay/
toolbar.rs

1//! Replay toolbar UI
2//!
3//! Provide playback controls for market replay functionality including
4//! play/pause/stop buttons, speed selector, progress bar, and time display.
5
6use super::model::{
7    PlaybackSpeed, ReplayCommand, ReplayProgress, ToolbarReplayState as ReplayState,
8};
9use crate::icons::icons;
10use crate::localization as l10n;
11use crate::theme::ThemeExt;
12use crate::tokens::DESIGN_TOKENS;
13use crate::ui_kit::buttons::{ButtonSize, IconButton};
14use crate::ui_kit::ext::UiExt;
15use egui::{Response, Ui, Vec2, Widget};
16use std::sync::mpsc::Sender;
17
18/// Replay toolbar configuration
19#[derive(Debug, Clone)]
20pub struct ReplayToolbarConfig {
21    /// Show speed selector
22    pub show_speed_selector: bool,
23    /// Show progress bar
24    pub show_progress: bool,
25    /// Show time display
26    pub show_time: bool,
27    /// Compact mode (icons only)
28    pub compact: bool,
29}
30
31impl Default for ReplayToolbarConfig {
32    fn default() -> Self {
33        Self {
34            show_speed_selector: true,
35            show_progress: true,
36            show_time: true,
37            compact: false,
38        }
39    }
40}
41
42/// Replay toolbar state
43#[derive(Debug, Clone, Default)]
44pub struct ReplayToolbarState {
45    /// Current replay state
46    pub replay_state: ReplayState,
47    /// Current speed
48    pub speed: PlaybackSpeed,
49    /// Current progress (if available)
50    pub progress: Option<ReplayProgress>,
51}
52
53impl ReplayToolbarState {
54    /// Sync toolbar replay state from external values.
55    ///
56    /// `active` / `playing` map to the toolbar-level replay state enum,
57    /// and `speed_multiplier` is converted to the closest `PlaybackSpeed`.
58    pub fn sync_from(&mut self, active: bool, playing: bool, speed_multiplier: f32) {
59        if active {
60            if playing {
61                self.replay_state = ReplayState::Playing;
62            } else {
63                self.replay_state = ReplayState::Paused;
64            }
65        } else {
66            self.replay_state = ReplayState::Stopped;
67        }
68
69        self.speed = match speed_multiplier {
70            s if s <= 1.0 => PlaybackSpeed::RealTime,
71            s if s <= 4.0 => PlaybackSpeed::Fast,
72            s if s <= 100.0 => PlaybackSpeed::VeryFast,
73            _ => PlaybackSpeed::UltraFast,
74        };
75    }
76}
77
78/// Action emitted by the replay toolbar
79#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub enum ReplayToolbarAction {
82    /// Play was clicked
83    Play,
84    /// Pause was clicked
85    Pause,
86    /// Stop was clicked
87    Stop,
88    /// Speed was changed
89    SetSpeed(PlaybackSpeed),
90    /// Step forward
91    StepForward,
92    /// Step backward
93    StepBackward,
94    /// Open replay settings dialog
95    OpenSettings,
96}
97
98/// Replay toolbar widget
99pub struct ReplayToolbar<'a> {
100    state: &'a ReplayToolbarState,
101    config: ReplayToolbarConfig,
102    command_sender: Option<&'a Sender<ReplayCommand>>,
103}
104
105impl<'a> ReplayToolbar<'a> {
106    /// Create a new replay toolbar
107    pub fn new(state: &'a ReplayToolbarState) -> Self {
108        Self {
109            state,
110            config: ReplayToolbarConfig::default(),
111            command_sender: None,
112        }
113    }
114
115    /// Set configuration
116    pub fn with_config(mut self, config: ReplayToolbarConfig) -> Self {
117        self.config = config;
118        self
119    }
120
121    /// Set command sender for direct control
122    pub fn with_command_sender(mut self, sender: &'a Sender<ReplayCommand>) -> Self {
123        self.command_sender = Some(sender);
124        self
125    }
126
127    /// Enable compact mode
128    pub fn compact(mut self) -> Self {
129        self.config.compact = true;
130        self
131    }
132
133    /// Show the toolbar and return any action
134    pub fn show(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
135        let action = ui
136            .horizontal(|ui| {
137                ui.spacing_mut().item_spacing.x = ui.active_theme().spacing.sm;
138
139                let mut action = None;
140
141                // Play/Pause button
142                action = self.show_play_pause_button(ui).or(action);
143
144                // Stop button
145                action = self.show_stop_button(ui).or(action);
146
147                ui.space_sm();
148
149                // Step buttons
150                action = self.show_step_buttons(ui).or(action);
151
152                ui.space_md();
153
154                // Speed selector
155                if self.config.show_speed_selector {
156                    action = self.show_speed_selector(ui).or(action);
157                }
158
159                ui.space_md();
160
161                // Progress bar
162                if self.config.show_progress {
163                    self.show_progress_bar(ui);
164                }
165
166                // Time display
167                if self.config.show_time {
168                    self.show_time_display(ui);
169                }
170
171                // Settings button
172                if IconButton::new(&icons::SETTINGS)
173                    .size(self.button_size())
174                    .tooltip("Settings")
175                    .show(ui)
176                    .clicked()
177                {
178                    action = Some(ReplayToolbarAction::OpenSettings);
179                }
180
181                action
182            })
183            .inner;
184
185        // Send command if we have a sender
186        if let (Some(sender), Some(act)) = (&self.command_sender, &action) {
187            let cmd = match act {
188                ReplayToolbarAction::Play => Some(ReplayCommand::Resume),
189                ReplayToolbarAction::Pause => Some(ReplayCommand::Pause),
190                ReplayToolbarAction::Stop => Some(ReplayCommand::Stop),
191                ReplayToolbarAction::SetSpeed(s) => Some(ReplayCommand::SetSpeed(*s)),
192                ReplayToolbarAction::StepForward => Some(ReplayCommand::StepForward),
193                ReplayToolbarAction::StepBackward => Some(ReplayCommand::StepBackward),
194                ReplayToolbarAction::OpenSettings => None,
195            };
196
197            if let Some(cmd) = cmd {
198                let _ = sender.send(cmd);
199            }
200        }
201
202        action
203    }
204
205    fn button_size(&self) -> ButtonSize {
206        if self.config.compact {
207            ButtonSize::SM
208        } else {
209            ButtonSize::MD
210        }
211    }
212
213    fn show_play_pause_button(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
214        let (icon, tooltip, action) = match self.state.replay_state {
215            ReplayState::Playing => (&icons::MEDIA_PAUSE, "Pause", ReplayToolbarAction::Pause),
216            ReplayState::Paused | ReplayState::Stopped | ReplayState::Finished => {
217                (&icons::MEDIA_PLAY, "Play", ReplayToolbarAction::Play)
218            }
219            ReplayState::Buffering => (
220                &icons::UI_BUFFERING,
221                "Buffering…",
222                ReplayToolbarAction::Pause,
223            ),
224            ReplayState::Error => (&icons::STATUS_ERROR, "Error", ReplayToolbarAction::Stop),
225        };
226
227        let response = IconButton::new(icon)
228            .size(self.button_size())
229            .tooltip(tooltip)
230            .show(ui);
231
232        if response.clicked() {
233            Some(action)
234        } else {
235            None
236        }
237    }
238
239    fn show_stop_button(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
240        let response = IconButton::new(&icons::MEDIA_STOP)
241            .size(self.button_size())
242            .tooltip("Stop")
243            .enabled(self.state.replay_state.is_active())
244            .show(ui);
245
246        if response.clicked() {
247            Some(ReplayToolbarAction::Stop)
248        } else {
249            None
250        }
251    }
252
253    fn show_step_buttons(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
254        let can_step =
255            self.state.replay_state == ReplayState::Paused || self.state.replay_state.can_start();
256
257        let mut action = None;
258
259        if IconButton::new(&icons::UI_ARROW_BACK)
260            .size(self.button_size())
261            .tooltip("Step backward")
262            .enabled(can_step)
263            .show(ui)
264            .clicked()
265        {
266            action = Some(ReplayToolbarAction::StepBackward);
267        }
268
269        if IconButton::new(&icons::UI_ARROW_FORWARD)
270            .size(self.button_size())
271            .tooltip("Step forward")
272            .enabled(can_step)
273            .show(ui)
274            .clicked()
275        {
276            action = Some(ReplayToolbarAction::StepForward);
277        }
278
279        action
280    }
281
282    fn show_speed_selector(&self, ui: &mut Ui) -> Option<ReplayToolbarAction> {
283        let mut action = None;
284        let current = self.state.speed;
285
286        egui::ComboBox::from_id_salt("replay_speed")
287            .selected_text(l10n::text(ui.ctx(), current.display_name()))
288            .width(DESIGN_TOKENS.sizing.replay.speed_dropdown_width)
289            .show_ui(ui, |ui| {
290                for speed in [
291                    PlaybackSpeed::RealTime,
292                    PlaybackSpeed::Fast,
293                    PlaybackSpeed::VeryFast,
294                    PlaybackSpeed::UltraFast,
295                ] {
296                    if ui
297                        .selectable_value(
298                            &mut self.state.speed.clone(),
299                            speed,
300                            l10n::text(ui.ctx(), speed.display_name()),
301                        )
302                        .clicked()
303                        && speed != current
304                    {
305                        action = Some(ReplayToolbarAction::SetSpeed(speed));
306                    }
307                }
308            });
309
310        action
311    }
312
313    fn show_progress_bar(&self, ui: &mut Ui) {
314        if let Some(ref progress) = self.state.progress {
315            let pct = progress.progress_pct() as f32;
316            let theme = ui.active_theme();
317
318            // Progress bar
319            let desired_size = Vec2::new(
320                DESIGN_TOKENS.sizing.replay.progress_bar_width / 2.0,
321                theme.spacing.sm,
322            );
323            let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover());
324            let translated_label = l10n::text(ui.ctx(), "Replay progress");
325            response.widget_info(|| {
326                let mut info = egui::WidgetInfo::labeled(
327                    egui::Role::ProgressIndicator,
328                    response.enabled(),
329                    &translated_label,
330                );
331                info.value = Some(f64::from(pct) * 100.0);
332                info
333            });
334
335            if ui.is_rect_visible(rect) {
336                let painter = ui.painter();
337                let (bg, fg) = (theme.chart.bg, theme.ui.accent);
338
339                // Background
340                painter.rect_filled(rect, ui.active_theme().radius.xs, bg);
341
342                // Progress fill
343                let fill_width = rect.width() * pct;
344                let fill_rect =
345                    egui::Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height()));
346                painter.rect_filled(fill_rect, ui.active_theme().radius.xs, fg);
347            }
348
349            // Beside the bar, never over the fill.
350            ui.label(
351                egui::RichText::new(format!("{:.0}%", pct * 100.0))
352                    .size(theme.text.small)
353                    .color(theme.ui.text_secondary),
354            );
355        }
356    }
357
358    fn show_time_display(&self, ui: &mut Ui) {
359        if let Some(ref progress) = self.state.progress {
360            let time_str = progress.current_time.format("%H:%M:%S").to_string();
361            ui.label(
362                egui::RichText::new(l10n::text(ui.ctx(), time_str))
363                    .monospace()
364                    .small(),
365            );
366
367            // Bars played
368            let bars_str = format!("{}/{}", progress.bars_played, progress.total_bars);
369            ui.hint_label(bars_str);
370        }
371    }
372}
373
374impl Widget for ReplayToolbar<'_> {
375    fn ui(self, ui: &mut Ui) -> Response {
376        ui.horizontal(|ui| {
377            self.show(ui);
378        })
379        .response
380    }
381}