1use super::{ReplayAction, ReplayColors, ReplayConfig, ReplaySpeed, ReplayState};
6use crate::icons::icons;
7use crate::localization as l10n;
8use crate::styles::stroke;
9use crate::theme::ThemeExt;
10use crate::tokens::DESIGN_TOKENS;
11use crate::ui_kit::buttons::{Button, ButtonSize, IconButton};
12use crate::ui_kit::ext::UiExt;
13use egui::{RichText, Ui, Vec2};
14
15fn colors(config: &ReplayConfig, ui: &Ui) -> ReplayColors {
17 if config.colors == ReplayColors::default() {
18 ReplayColors::from_theme(&ui.active_theme())
19 } else {
20 config.colors.clone()
21 }
22}
23
24fn kit_size(points: f32) -> ButtonSize {
26 [
27 ButtonSize::SM,
28 ButtonSize::MD,
29 ButtonSize::LG,
30 ButtonSize::XL,
31 ]
32 .into_iter()
33 .min_by(|a, b| {
34 (a.pixels() - points)
35 .abs()
36 .total_cmp(&(b.pixels() - points).abs())
37 })
38 .unwrap_or_default()
39}
40
41pub struct ReplayControls<'a> {
61 config: &'a ReplayConfig,
62}
63
64impl<'a> ReplayControls<'a> {
65 pub fn new(config: &'a ReplayConfig) -> Self {
67 Self { config }
68 }
69
70 pub fn show(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
72 let _bars_advanced = state.update();
74
75 let layout = self.config.layout.resolved(&ui.active_theme());
76 let mut result_action = ReplayAction::None;
77
78 ui.horizontal(|ui| {
79 ui.set_height(layout.control_bar_height);
80 ui.add_space(layout.padding);
81
82 if let action @ ReplayAction::ToggleReplayMode = self.render_mode_toggle(ui, state) {
84 result_action = action;
85 }
86
87 ui.add_space(layout.btn_spacing);
88 ui.separator();
89 ui.add_space(layout.btn_spacing);
90
91 if state.is_active() {
93 let playback_action = self.render_playback_controls(ui, state);
95 if !matches!(playback_action, ReplayAction::None) {
96 result_action = playback_action;
97 }
98
99 ui.add_space(layout.btn_spacing);
100 ui.separator();
101 ui.add_space(layout.btn_spacing);
102
103 let progress_action = self.render_progress_bar(ui, state);
105 if !matches!(progress_action, ReplayAction::None) {
106 result_action = progress_action;
107 }
108
109 ui.add_space(layout.btn_spacing);
110 ui.separator();
111 ui.add_space(layout.btn_spacing);
112
113 let speed_action = self.render_speed_control(ui, state);
115 if !matches!(speed_action, ReplayAction::None) {
116 result_action = speed_action;
117 }
118
119 ui.add_space(layout.btn_spacing);
120 ui.separator();
121 ui.add_space(layout.btn_spacing);
122
123 self.render_info_display(ui, state);
125 } else {
126 ui.label(
128 RichText::new(l10n::text(ui.ctx(), "Click Replay to enter replay mode"))
129 .color(colors(self.config, ui).disabled_color),
130 );
131 }
132
133 ui.add_space(layout.padding);
134 });
135
136 result_action
137 }
138
139 fn render_mode_toggle(&self, ui: &mut Ui, state: &ReplayState) -> ReplayAction {
141 let btn_text = if state.is_active() {
142 "Exit replay"
143 } else {
144 "Replay"
145 };
146 let button = Button::new(btn_text)
147 .secondary()
148 .size(kit_size(self.config.layout.btn_size));
149
150 if button.show(ui).clicked() {
151 ReplayAction::ToggleReplayMode
152 } else {
153 ReplayAction::None
154 }
155 }
156
157 fn render_playback_controls(&self, ui: &mut Ui, state: &ReplayState) -> ReplayAction {
159 let layout = self.config.layout.resolved(&ui.active_theme());
160 let small = kit_size(layout.small_button_size);
161 let mut action = ReplayAction::None;
162
163 if IconButton::new(&icons::UI_ARROW_BACK)
164 .size(small)
165 .tooltip("Step backward (Left arrow)")
166 .enabled(!state.at_start())
167 .show(ui)
168 .clicked()
169 {
170 action = ReplayAction::StepBackward;
171 }
172
173 ui.add_space(layout.btn_spacing);
174
175 let (play_pause_icon, play_pause_hint) = if state.is_playing() {
176 (&icons::MEDIA_PAUSE, "Pause (Space)")
177 } else {
178 (&icons::MEDIA_PLAY, "Play (Space)")
179 };
180 if IconButton::new(play_pause_icon)
181 .size(kit_size(layout.btn_size))
182 .tooltip(play_pause_hint)
183 .show(ui)
184 .clicked()
185 {
186 action = ReplayAction::TogglePlayPause;
187 }
188
189 ui.add_space(layout.btn_spacing);
190
191 if IconButton::new(&icons::UI_ARROW_FORWARD)
192 .size(small)
193 .tooltip("Step forward (Right arrow)")
194 .enabled(!state.at_end())
195 .show(ui)
196 .clicked()
197 {
198 action = ReplayAction::StepForward;
199 }
200
201 ui.add_space(layout.btn_spacing);
202
203 if IconButton::new(&icons::MEDIA_STOP)
204 .size(small)
205 .tooltip("Reset to start")
206 .show(ui)
207 .clicked()
208 {
209 action = ReplayAction::Reset;
210 }
211
212 action
213 }
214
215 fn render_progress_bar(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
217 let layout = self.config.layout.resolved(&ui.active_theme());
218 let colors = colors(self.config, ui);
219 let mut action = ReplayAction::None;
220
221 let progress = state.progress();
222 let width = layout
223 .progress_bar_width
224 .unwrap_or(DESIGN_TOKENS.sizing.replay.progress_bar_width);
225
226 let (rect, response) = ui.allocate_exact_size(
227 Vec2::new(width, layout.progress_bar_height),
228 egui::Sense::click_and_drag(),
229 );
230 crate::styles::focus::draw_focus_ring(ui, &response, ui.active_theme().radius.xs);
231 response.widget_info(|| {
232 egui::WidgetInfo::slider(
233 response.enabled(),
234 f64::from(progress) * 100.0,
235 "Replay position",
236 )
237 });
238
239 if ui.is_rect_visible(rect) {
240 ui.painter()
242 .rect_filled(rect, ui.active_theme().radius.xs, colors.progress_bg);
243
244 let filled_width = rect.width() * progress;
246 let filled_rect =
247 egui::Rect::from_min_size(rect.min, Vec2::new(filled_width, rect.height()));
248 ui.painter().rect_filled(
249 filled_rect,
250 ui.active_theme().radius.xs,
251 colors.progress_fill,
252 );
253
254 let cursor_x = rect.min.x + filled_width;
256 let cursor_center = egui::pos2(cursor_x, rect.center().y);
257 ui.painter().circle_filled(
258 cursor_center,
259 layout.progress_bar_height * 0.8,
260 colors.progress_cursor,
261 );
262
263 for marker in &state.markers {
265 if state.total_bars > 1 {
266 let marker_progress = marker.bar_idx as f32 / (state.total_bars - 1) as f32;
267 let marker_x = rect.min.x + rect.width() * marker_progress;
268 let marker_color = egui::Color32::from_rgba_unmultiplied(
269 marker.color[0],
270 marker.color[1],
271 marker.color[2],
272 marker.color[3],
273 );
274 ui.painter().line_segment(
275 [
276 egui::pos2(marker_x, rect.min.y),
277 egui::pos2(marker_x, rect.max.y),
278 ],
279 egui::Stroke::new(stroke::MEDIUM, marker_color),
280 );
281 }
282 }
283 }
284
285 if (response.clicked() || response.dragged())
287 && let Some(pos) = response.interact_pointer_pos()
288 {
289 let relative_x = (pos.x - rect.min.x) / rect.width();
290 let new_progress = relative_x.clamp(0.0, 1.0);
291 action = ReplayAction::JumpToPercent(new_progress);
292 }
293
294 response.on_hover_text(l10n::format_text(
296 ui.ctx(),
297 "Bar {0} / {1} ({2}%)",
298 &[
299 ("0", format!("{}", state.curr_bar + 1)),
300 ("1", format!("{}", state.total_bars)),
301 ("2", format!("{:.1}", progress * 100.0)),
302 ],
303 ));
304
305 action
306 }
307
308 fn render_speed_control(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
310 let layout = self.config.layout.resolved(&ui.active_theme());
311 let colors = colors(self.config, ui);
312 let small = kit_size(layout.small_button_size);
313 let mut action = ReplayAction::None;
314
315 if IconButton::new(&icons::MINUS)
316 .size(small)
317 .tooltip("Slow down")
318 .show(ui)
319 .clicked()
320 {
321 action = ReplayAction::SlowDown;
322 }
323
324 let speed_label = state.speed.label();
326 let combo_id = ui.scope_id().with("speed_combo");
327
328 egui::ComboBox::from_id_salt(combo_id)
329 .selected_text(
330 RichText::new(l10n::text(ui.ctx(), &speed_label)).color(colors.speed_color),
331 )
332 .width(layout.speed_dropdown_width)
333 .show_ui(ui, |ui| {
334 for preset in ReplaySpeed::presets() {
335 let label = preset.label();
336 if ui
337 .selectable_label(*preset == state.speed, l10n::text(ui.ctx(), &label))
338 .clicked()
339 {
340 action = ReplayAction::SetSpeed(preset.multiplier());
341 }
342 }
343 });
344
345 if IconButton::new(&icons::PLUS)
346 .size(small)
347 .tooltip("Speed up")
348 .show(ui)
349 .clicked()
350 {
351 action = ReplayAction::SpeedUp;
352 }
353
354 action
355 }
356
357 fn render_info_display(&self, ui: &mut Ui, state: &ReplayState) {
359 let behavior = &self.config.behavior;
360 let layout = self.config.layout.resolved(&ui.active_theme());
361
362 ui.right_aligned(|ui| {
363 if behavior.show_bar_counter {
365 ui.label(
366 RichText::new(format!("{}/{}", state.curr_bar + 1, state.total_bars)).small(),
367 );
368 }
369
370 if let Some(date) = state.curr_date {
372 ui.add_space(layout.btn_spacing);
373 ui.label(
374 RichText::new(l10n::text(
375 ui.ctx(),
376 date.format("%Y-%m-%d %H:%M").to_string(),
377 ))
378 .small(),
379 );
380 }
381
382 if let Some(ref symbol) = state.symbol {
384 ui.add_space(layout.btn_spacing);
385 ui.strong_label(symbol);
386 }
387 });
388 }
389}
390
391pub struct CompactReplayControls<'a> {
393 config: &'a ReplayConfig,
394}
395
396impl<'a> CompactReplayControls<'a> {
397 pub fn new(config: &'a ReplayConfig) -> Self {
399 Self { config }
400 }
401
402 pub fn show(&self, ui: &mut Ui, state: &mut ReplayState) -> ReplayAction {
404 let mut action = ReplayAction::None;
405 let colors = colors(self.config, ui);
406
407 let _bars = state.update();
409
410 ui.horizontal(|ui| {
411 let (icon, hint) = if state.is_playing() {
412 (&icons::MEDIA_PAUSE, "Pause (Space)")
413 } else {
414 (&icons::MEDIA_PLAY, "Play (Space)")
415 };
416 if IconButton::new(icon)
417 .size(ButtonSize::SM)
418 .tooltip(hint)
419 .show(ui)
420 .clicked()
421 {
422 action = ReplayAction::TogglePlayPause;
423 }
424
425 let progress = state.progress();
427 let progress_bar = egui::ProgressBar::new(progress)
428 .show_percentage()
429 .fill(colors.progress_fill);
430 let response = ui.add(progress_bar);
431
432 if response.clicked()
434 && let Some(pos) = response.interact_pointer_pos()
435 {
436 let rect = response.rect;
437 let relative_x = (pos.x - rect.min.x) / rect.width();
438 action = ReplayAction::JumpToPercent(relative_x.clamp(0.0, 1.0));
439 }
440
441 ui.label(
443 RichText::new(l10n::text(ui.ctx(), state.speed.label()))
444 .small()
445 .color(colors.speed_color),
446 );
447 });
448
449 action
450 }
451}
452
453pub struct ReplayKeyboardHandler;
455
456impl ReplayKeyboardHandler {
457 pub fn handle(ui: &Ui, state: &ReplayState) -> ReplayAction {
459 if !state.is_active() {
460 return ReplayAction::None;
461 }
462
463 if ui.ctx().memory(|mem| mem.focused().is_some()) {
465 return ReplayAction::None;
466 }
467
468 ui.input(|i| {
469 if i.key_pressed(egui::Key::Space) {
471 return ReplayAction::TogglePlayPause;
472 }
473
474 if i.key_pressed(egui::Key::ArrowLeft) {
476 if i.modifiers.shift {
477 return ReplayAction::StepBackwardN(10);
478 }
479 return ReplayAction::StepBackward;
480 }
481
482 if i.key_pressed(egui::Key::ArrowRight) {
484 if i.modifiers.shift {
485 return ReplayAction::StepForwardN(10);
486 }
487 return ReplayAction::StepForward;
488 }
489
490 if i.key_pressed(egui::Key::ArrowUp) {
492 return ReplayAction::SpeedUp;
493 }
494
495 if i.key_pressed(egui::Key::ArrowDown) {
497 return ReplayAction::SlowDown;
498 }
499
500 if i.key_pressed(egui::Key::Home) {
502 return ReplayAction::JumpToStart;
503 }
504
505 if i.key_pressed(egui::Key::End) {
507 return ReplayAction::JumpToEnd;
508 }
509
510 if i.key_pressed(egui::Key::M) {
512 return ReplayAction::AddMarker {
513 label: None,
514 color: None,
515 };
516 }
517
518 if i.key_pressed(egui::Key::R) && i.modifiers.ctrl {
520 return ReplayAction::Reset;
521 }
522
523 if i.key_pressed(egui::Key::Escape) {
525 return ReplayAction::ExitReplayMode;
526 }
527
528 ReplayAction::None
529 })
530 }
531}