Skip to main content

libsurfer/
clock_highlighting.rs

1//! Drawing and handling of clock highlighting.
2use 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
12/// Base period, in screen pixels, used to interleave dashed clock-edge lines.
13///
14/// For coincident edges from N clocks, each clock gets a dash length of
15/// `CLOCK_LINE_BASE_UNIT / N` and a corresponding phase offset so all dashes tile
16/// the same period without overlap.
17const CLOCK_LINE_BASE_UNIT: f32 = 30.0;
18
19/// Cached clock-highlight payload for the active highlight strategy.
20///
21/// The payload stores data in the shape most efficient for the selected
22/// `ClockHighlightType` and includes `active_clock_count` metadata to avoid
23/// recomputing active-clock state while drawing.
24pub(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    /// Returns whether this cached highlight payload represents any active clocks.
38    ///
39    /// The value is derived from `active_clock_count` metadata captured during draw-command
40    /// generation so rendering can skip additional scans.
41    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
54/// Selects a stable highlight color for a clock index.
55///
56/// If only one clock is active, the base fallback color is always used.
57/// For multi-clock rendering, the color cycle is: fallback, then `color_list` entries.
58fn 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    /// Draw a line at every posedge of the clocks
77    Line,
78
79    /// Highlight every other cycle
80    Cycle,
81
82    /// No highlighting
83    None,
84}
85
86/// Draws clock highlight marks for the currently selected highlight mode.
87///
88/// `Line` mode draws vertical lines (or interleaved dashes for coincident edges), while
89/// `Cycle` mode paints alternating cycle spans for each active clock.
90pub(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            // Process clock edges in pairs: every other cycle gets highlighted
152            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
199/// Renders the UI radio options for selecting the clock highlight type.
200pub(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    /// Builds cached clock-highlight data for the active highlight mode.
220    ///
221    /// The input uses sparse clock indices so color assignment remains stable relative to
222    /// original clock order, and `active_clock_count` is stored for fast rendering decisions.
223    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
243/// Groups sparse per-clock edge lists into time-ordered `(time, clock_indices)` tuples.
244///
245/// This structure is used by `Line` mode to interleave coincident edges at the same time.
246fn 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
268/// Converts sparse `(clock_idx, edges)` pairs into a dense vector indexed by clock index.
269///
270/// Missing indices are represented by empty edge lists.
271fn 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}