1use derive_more::{Display, FromStr};
3use ecolor::Color32;
4use egui::Ui;
5use emath::{Pos2, Rect};
6use enum_iterator::Sequence;
7use epaint::{CornerRadius, Shape, Stroke};
8use serde::{Deserialize, Serialize};
9
10use crate::{SystemState, config::SurferConfig, message::Message, view::DrawingContext};
11
12const CLOCK_LINE_BASE_UNIT: f32 = 30.0;
18
19pub(crate) enum ClockHighlightData {
25 Line {
26 clock_edges: Vec<(f32, Vec<usize>)>,
27 active_clock_count: usize,
28 },
29 Cycle {
30 clock_edges_by_clock: Vec<Vec<f32>>,
31 active_clock_count: usize,
32 },
33 None,
34}
35
36impl ClockHighlightData {
37 pub(crate) fn has_edges(&self) -> bool {
42 match self {
43 Self::Line {
44 active_clock_count, ..
45 }
46 | Self::Cycle {
47 active_clock_count, ..
48 } => *active_clock_count > 0,
49 Self::None => false,
50 }
51 }
52}
53
54fn clock_highlight_color(
59 clock_idx: usize,
60 single_active_clock: bool,
61 fallback_color: Color32,
62 color_list: &[Color32],
63) -> Color32 {
64 if single_active_clock {
65 fallback_color
66 } else {
67 match clock_idx % (color_list.len() + 1) {
68 0 => fallback_color,
69 color_idx => color_list[color_idx - 1],
70 }
71 }
72}
73
74#[derive(PartialEq, Copy, Clone, Debug, Deserialize, Display, FromStr, Sequence, Serialize)]
75pub enum ClockHighlightType {
76 Line,
78
79 Cycle,
81
82 None,
84}
85
86pub(crate) fn draw_clock_edge_marks(
91 clock_edges: &ClockHighlightData,
92 ctx: &mut DrawingContext,
93 config: &SurferConfig,
94) {
95 match clock_edges {
96 ClockHighlightData::Line {
97 clock_edges,
98 active_clock_count,
99 } => {
100 let y_start = (ctx.to_screen)(0., 0.).y;
101 let y_end = y_start + ctx.cfg.canvas_size.y;
102 let stroke_width = config.theme.clock_highlight_line.width;
103 let fallback_color = config.theme.clock_highlight_line.color;
104 let color_list = &config.theme.clock_highlight_line_colors;
105 let single_active_clock = *active_clock_count <= 1;
106
107 for (x, clock_indices) in clock_edges {
108 let x_pos = (ctx.to_screen)(*x, 0.).x;
109
110 if clock_indices.len() == 1 {
111 let clock_idx = clock_indices[0];
112 let stroke_color = clock_highlight_color(
113 clock_idx,
114 single_active_clock,
115 fallback_color,
116 color_list,
117 );
118 let stroke = Stroke::new(stroke_width, stroke_color);
119 ctx.painter.vline(x_pos, y_start..=y_end, stroke);
120 continue;
121 }
122
123 let dash_length = CLOCK_LINE_BASE_UNIT / clock_indices.len() as f32;
124 let gap_length = CLOCK_LINE_BASE_UNIT - dash_length;
125
126 let line = [Pos2::new(x_pos, y_start), Pos2::new(x_pos, y_end)];
127 for (phase_idx, clock_idx) in clock_indices.iter().copied().enumerate() {
128 let stroke_color = clock_highlight_color(
129 clock_idx,
130 single_active_clock,
131 fallback_color,
132 color_list,
133 );
134 let stroke = Stroke::new(stroke_width, stroke_color);
135 let offset = phase_idx as f32 * dash_length;
136
137 ctx.painter.add(Shape::dashed_line_with_offset(
138 &line,
139 stroke,
140 &[dash_length],
141 &[gap_length],
142 offset,
143 ));
144 }
145 }
146 }
147 ClockHighlightData::Cycle {
148 clock_edges_by_clock,
149 active_clock_count,
150 } => {
151 let fallback_fill_color = config.theme.clock_highlight_cycle;
153 let color_list = &config.theme.clock_highlight_cycle_colors;
154 let single_active_clock = *active_clock_count <= 1;
155
156 for (clock_idx, clock_edges) in clock_edges_by_clock.iter().enumerate() {
157 let fill_color = clock_highlight_color(
158 clock_idx,
159 single_active_clock,
160 fallback_fill_color,
161 color_list,
162 );
163 let fill_color = if single_active_clock {
164 fill_color
165 } else {
166 egui::Color32::from_rgba_unmultiplied(
167 fill_color.r(),
168 fill_color.g(),
169 fill_color.b(),
170 128,
171 )
172 };
173
174 for chunk in clock_edges.chunks(2) {
175 if let [x_start, x_end] = chunk {
176 let Pos2 {
177 x: x_end_screen,
178 y: y_start,
179 } = (ctx.to_screen)(*x_end, 0.);
180 ctx.painter.rect_filled(
181 Rect {
182 min: (ctx.to_screen)(*x_start, 0.),
183 max: Pos2 {
184 x: x_end_screen,
185 y: ctx.cfg.canvas_size.y + y_start,
186 },
187 },
188 CornerRadius::ZERO,
189 fill_color,
190 );
191 }
192 }
193 }
194 }
195 ClockHighlightData::None => (),
196 }
197}
198
199pub(crate) fn clock_highlight_type_menu(
201 ui: &mut Ui,
202 msgs: &mut Vec<Message>,
203 clock_highlight_type: ClockHighlightType,
204) {
205 for highlight_type in enum_iterator::all::<ClockHighlightType>() {
206 if ui
207 .radio(
208 highlight_type == clock_highlight_type,
209 highlight_type.to_string(),
210 )
211 .clicked()
212 {
213 msgs.push(Message::SetClockHighlightType(highlight_type));
214 }
215 }
216}
217
218impl SystemState {
219 pub(crate) fn get_clock_hightlight_data(
224 &self,
225 clock_edges_by_clock: Vec<(usize, Vec<f32>)>,
226 ) -> ClockHighlightData {
227 let active_clock_count = clock_edges_by_clock.len();
228
229 match self.clock_highlight_type() {
230 ClockHighlightType::Line => ClockHighlightData::Line {
231 clock_edges: group_clock_edges_by_time(clock_edges_by_clock),
232 active_clock_count,
233 },
234 ClockHighlightType::Cycle => ClockHighlightData::Cycle {
235 clock_edges_by_clock: dense_clock_edges_by_clock(clock_edges_by_clock),
236 active_clock_count,
237 },
238 ClockHighlightType::None => ClockHighlightData::None,
239 }
240 }
241}
242
243fn group_clock_edges_by_time(
247 clock_edges_by_clock: Vec<(usize, Vec<f32>)>,
248) -> Vec<(f32, Vec<usize>)> {
249 let mut flattened = clock_edges_by_clock
250 .into_iter()
251 .flat_map(|(clock_idx, edges)| edges.into_iter().map(move |x| (x, clock_idx)))
252 .collect::<Vec<_>>();
253 flattened.sort_by(|(x1, _), (x2, _)| x1.total_cmp(x2));
254
255 let mut grouped = Vec::<(f32, Vec<usize>)>::new();
256 for (x, clock_idx) in flattened {
257 if let Some((last_x, clock_indices)) = grouped.last_mut()
258 && *last_x == x
259 {
260 clock_indices.push(clock_idx);
261 } else {
262 grouped.push((x, vec![clock_idx]));
263 }
264 }
265 grouped
266}
267
268fn dense_clock_edges_by_clock(clock_edges_by_clock: Vec<(usize, Vec<f32>)>) -> Vec<Vec<f32>> {
272 let max_clock_idx = clock_edges_by_clock
273 .iter()
274 .map(|(clock_idx, _)| *clock_idx)
275 .max()
276 .map_or(0, |max_idx| max_idx + 1);
277 let mut dense_clock_edges = vec![Vec::<f32>::new(); max_clock_idx];
278
279 for (clock_idx, edges) in clock_edges_by_clock {
280 dense_clock_edges[clock_idx] = edges;
281 }
282
283 dense_clock_edges
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn clock_highlight_data_has_edges_uses_active_clock_count() {
292 let line = ClockHighlightData::Line {
293 clock_edges: vec![(1.0, vec![0])],
294 active_clock_count: 1,
295 };
296 assert!(line.has_edges());
297
298 let line_inactive = ClockHighlightData::Line {
299 clock_edges: vec![(1.0, vec![0])],
300 active_clock_count: 0,
301 };
302 assert!(!line_inactive.has_edges());
303
304 let cycle = ClockHighlightData::Cycle {
305 clock_edges_by_clock: vec![vec![2.0]],
306 active_clock_count: 1,
307 };
308 assert!(cycle.has_edges());
309
310 let cycle_inactive = ClockHighlightData::Cycle {
311 clock_edges_by_clock: vec![vec![2.0]],
312 active_clock_count: 0,
313 };
314 assert!(!cycle_inactive.has_edges());
315
316 assert!(!ClockHighlightData::None.has_edges());
317 }
318
319 #[test]
320 fn clock_highlight_color_uses_fallback_for_single_clock() {
321 let fallback = egui::Color32::from_rgb(1, 2, 3);
322 let list = [egui::Color32::from_rgb(10, 20, 30)];
323
324 assert_eq!(clock_highlight_color(0, true, fallback, &list), fallback);
325 assert_eq!(clock_highlight_color(5, true, fallback, &list), fallback);
326 }
327
328 #[test]
329 fn clock_highlight_color_cycles_fallback_and_list_for_multi_clock() {
330 let fallback = egui::Color32::from_rgb(1, 2, 3);
331 let c1 = egui::Color32::from_rgb(10, 20, 30);
332 let c2 = egui::Color32::from_rgb(40, 50, 60);
333 let list = [c1, c2];
334
335 assert_eq!(clock_highlight_color(0, false, fallback, &list), fallback);
336 assert_eq!(clock_highlight_color(1, false, fallback, &list), c1);
337 assert_eq!(clock_highlight_color(2, false, fallback, &list), c2);
338 assert_eq!(clock_highlight_color(3, false, fallback, &list), fallback);
339 }
340
341 #[test]
342 fn group_clock_edges_by_time_groups_and_sorts() {
343 let grouped = group_clock_edges_by_time(vec![
344 (2, vec![7.0, 1.0]),
345 (0, vec![4.0]),
346 (1, vec![1.0, 7.0]),
347 ]);
348
349 assert_eq!(
350 grouped,
351 vec![(1.0, vec![2, 1]), (4.0, vec![0]), (7.0, vec![2, 1])]
352 );
353 }
354
355 #[test]
356 fn dense_clock_edges_by_clock_keeps_sparse_indices() {
357 let dense = dense_clock_edges_by_clock(vec![(2, vec![3.0]), (0, vec![1.0, 2.0])]);
358
359 assert_eq!(dense.len(), 3);
360 assert_eq!(dense[0], vec![1.0, 2.0]);
361 assert_eq!(dense[1], Vec::<f32>::new());
362 assert_eq!(dense[2], vec![3.0]);
363 }
364}