1use 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#[derive(Debug, Clone)]
20pub struct ReplayToolbarConfig {
21 pub show_speed_selector: bool,
23 pub show_progress: bool,
25 pub show_time: bool,
27 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#[derive(Debug, Clone, Default)]
44pub struct ReplayToolbarState {
45 pub replay_state: ReplayState,
47 pub speed: PlaybackSpeed,
49 pub progress: Option<ReplayProgress>,
51}
52
53impl ReplayToolbarState {
54 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#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub enum ReplayToolbarAction {
82 Play,
84 Pause,
86 Stop,
88 SetSpeed(PlaybackSpeed),
90 StepForward,
92 StepBackward,
94 OpenSettings,
96}
97
98pub struct ReplayToolbar<'a> {
100 state: &'a ReplayToolbarState,
101 config: ReplayToolbarConfig,
102 command_sender: Option<&'a Sender<ReplayCommand>>,
103}
104
105impl<'a> ReplayToolbar<'a> {
106 pub fn new(state: &'a ReplayToolbarState) -> Self {
108 Self {
109 state,
110 config: ReplayToolbarConfig::default(),
111 command_sender: None,
112 }
113 }
114
115 pub fn with_config(mut self, config: ReplayToolbarConfig) -> Self {
117 self.config = config;
118 self
119 }
120
121 pub fn with_command_sender(mut self, sender: &'a Sender<ReplayCommand>) -> Self {
123 self.command_sender = Some(sender);
124 self
125 }
126
127 pub fn compact(mut self) -> Self {
129 self.config.compact = true;
130 self
131 }
132
133 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 action = self.show_play_pause_button(ui).or(action);
143
144 action = self.show_stop_button(ui).or(action);
146
147 ui.space_sm();
148
149 action = self.show_step_buttons(ui).or(action);
151
152 ui.space_md();
153
154 if self.config.show_speed_selector {
156 action = self.show_speed_selector(ui).or(action);
157 }
158
159 ui.space_md();
160
161 if self.config.show_progress {
163 self.show_progress_bar(ui);
164 }
165
166 if self.config.show_time {
168 self.show_time_display(ui);
169 }
170
171 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 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 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 painter.rect_filled(rect, ui.active_theme().radius.xs, bg);
341
342 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 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 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}