Skip to main content

launcher/widgets/
replay_row.rs

1use eframe::egui;
2use egui_extras::TableRow;
3use std::path::Path;
4use supreme_replay_analyzer::AnalyzeResult;
5
6pub enum ReplayRowAction {
7    CreateSnapshot,
8}
9
10pub fn render_replay_row(
11    row: &mut TableRow,
12    path: &Path,
13    result: &Result<AnalyzeResult, String>,
14    is_advanced_mode: bool,
15) -> Option<ReplayRowAction> {
16    let mut action = None;
17
18    // Column 1: Snapshot
19    row.col(|ui| {
20        if ui
21            .button("🔗")
22            .on_hover_text("Save and share replay")
23            .clicked()
24        {
25            action = Some(ReplayRowAction::CreateSnapshot);
26        }
27    });
28    // Column 2: File
29    row.col(|ui| {
30        let file_name = path.file_name().unwrap_or_default().to_string_lossy();
31        ui.label(file_name).on_hover_text(path.to_string_lossy());
32    });
33
34    match result {
35        Ok(analysis) => {
36            // Column 3: Player
37            row.col(|ui| {
38                ui.label(&analysis.player_name);
39            });
40            // Column 4: Track
41            row.col(|ui| {
42                ui.label(analysis.track_name.as_deref().unwrap_or("Unknown"));
43            });
44
45            if let Some(checkpoint_times) = &analysis.checkpoint_times {
46                // Column 5: Start
47                row.col(|ui| {
48                    display_checkpoint_time(
49                        ui,
50                        Some(analysis.start_ms),
51                        checkpoint_times.start_computed,
52                        is_advanced_mode,
53                    );
54                });
55
56                // Column 6: CP1
57                row.col(|ui| {
58                    display_checkpoint_time(
59                        ui,
60                        Some(analysis.checkpoint1_ms),
61                        checkpoint_times
62                            .cp1_computed
63                            .zip(checkpoint_times.start_computed)
64                            .map(|(cp, start)| cp - start),
65                        is_advanced_mode,
66                    );
67                });
68
69                // Column 7: CP2 (Hidden in simple mode)
70                if is_advanced_mode {
71                    row.col(|ui| {
72                        display_checkpoint_time(
73                            ui,
74                            None,
75                            checkpoint_times
76                                .cp2_computed
77                                .zip(checkpoint_times.start_computed)
78                                .map(|(cp, start)| cp - start),
79                            is_advanced_mode,
80                        );
81                    });
82                }
83
84                // Column 8: Finish
85                row.col(|ui| {
86                    display_checkpoint_time(
87                        ui,
88                        Some(analysis.displayed_ms),
89                        checkpoint_times
90                            .finish_computed
91                            .zip(checkpoint_times.start_computed)
92                            .map(|(finish, start)| finish - start),
93                        is_advanced_mode,
94                    );
95                });
96            } else {
97                // Column 5: Start (Fallback)
98                row.col(|ui| {
99                    display_checkpoint_time(ui, Some(analysis.start_ms), None, is_advanced_mode);
100                });
101
102                // Column 6: CP1 (Fallback)
103                row.col(|ui| {
104                    display_checkpoint_time(
105                        ui,
106                        Some(analysis.checkpoint1_ms),
107                        None,
108                        is_advanced_mode,
109                    );
110                });
111
112                // Column 7: CP2 (Fallback - always - as it's not in header)
113                if is_advanced_mode {
114                    row.col(|ui| {
115                        display_checkpoint_time(ui, None, None, is_advanced_mode);
116                    });
117                }
118
119                // Column 8: Finish (Fallback)
120                row.col(|ui| {
121                    display_checkpoint_time(
122                        ui,
123                        Some(analysis.displayed_ms),
124                        None,
125                        is_advanced_mode,
126                    );
127                });
128            }
129
130            // Column 9: Total time (from start to finish)
131            row.col(|ui| {
132                if !is_advanced_mode {
133                    ui.label(format_time(analysis.total_ms, 2));
134                } else if let Some(checkpoint_times) = &analysis.checkpoint_times
135                    && let Some(finish_time) = checkpoint_times.finish_computed
136                {
137                    display_checkpoint_time(
138                        ui,
139                        Some(analysis.total_ms),
140                        Some(finish_time),
141                        is_advanced_mode,
142                    );
143                } else {
144                    ui.label(format_time(analysis.total_ms, 2));
145                }
146            });
147
148            if is_advanced_mode {
149                // Lag Before
150                row.col(|ui| {
151                    ui.label(format_time(analysis.lag_start_ms, 2));
152                });
153                // Lag After
154                row.col(|ui| {
155                    ui.label(format_time(analysis.lag_finish_ms, 2));
156                });
157                // Total recording duration
158                row.col(|ui| {
159                    ui.label(format_time(analysis.recording_ms, 2));
160                });
161            }
162        }
163        Err(e) => {
164            row.col(|ui| {
165                ui.colored_label(egui::Color32::RED, "Error");
166            });
167            row.col(|ui| {
168                ui.colored_label(egui::Color32::RED, e);
169            });
170
171            // Simple: Start, CP1, Finish => 3
172            // Advanced: Start, CP1, CP2, Finish, Displayed, Total, LagBefore, LagAfter, Recording => 8
173            let remaining = if is_advanced_mode { 8 } else { 3 };
174
175            for _ in 0..remaining {
176                row.col(|ui| {
177                    ui.label("-");
178                });
179            }
180        }
181    }
182    action
183}
184
185fn display_checkpoint_time(
186    ui: &mut egui::Ui,
187    dat_time: Option<f32>,
188    computed_time: Option<f32>,
189    is_advanced_mode: bool,
190) {
191    // Helper: format .dat time (bold, 2 decimals)
192    let show_dat = |ui: &mut egui::Ui, dat: Option<f32>| {
193        match dat {
194            Some(t) => ui.label(egui::RichText::new(format_time(t, 2)).strong()),
195            None => ui.label(egui::RichText::new("-").strong()),
196        };
197    };
198
199    // Helper: format computed time (3 decimals, with delta if possible)
200    let show_computed = |ui: &mut egui::Ui, dat: Option<f32>, comp: Option<f32>| match (dat, comp) {
201        (Some(dat), Some(comp)) => {
202            let delta = comp - dat;
203            let sign = if delta >= 0.0 { "+" } else { "" };
204            ui.label(format!(
205                "{} ({sign}{:.3}s)",
206                format_time(comp, 3),
207                delta / 1000.0
208            ));
209        }
210        (None, Some(comp)) => {
211            ui.label(format_time(comp, 3));
212        }
213        _ => {}
214    };
215
216    if is_advanced_mode {
217        ui.vertical(|ui| {
218            show_dat(ui, dat_time);
219            show_computed(ui, dat_time, computed_time);
220        });
221    } else {
222        show_dat(ui, dat_time);
223    }
224}
225
226fn format_time(ms: f32, precision: usize) -> String {
227    let total_s = ms / 1000.0;
228    let abs_s = total_s.abs();
229    if abs_s >= 60.0 {
230        let minutes = (abs_s / 60.0).floor() as i32;
231        let seconds = abs_s % 60.0;
232        let sign = if total_s < 0.0 { "-" } else { "" };
233        if precision == 3 {
234            format!("{sign}{minutes}:{seconds:06.3}")
235        } else {
236            format!("{sign}{minutes}:{seconds:05.2}")
237        }
238    } else {
239        format!("{total_s:.precision$}s")
240    }
241}