ringkernel_procint/gui/canvas/
dfg_canvas.rs1use super::{Camera, NodePosition, TokenAnimation};
6use crate::gui::Theme;
7use crate::models::{DFGGraph, GpuDFGEdge, GpuDFGNode, GpuPatternMatch, PatternType};
8use eframe::egui::{self, Color32, Pos2, Rect, Stroke, Vec2};
9use std::collections::HashMap;
10
11pub struct DfgCanvas {
13 pub camera: Camera,
15 positions: HashMap<u32, NodePosition>,
17 activity_names: HashMap<u32, String>,
19 pub tokens: TokenAnimation,
21 selected_node: Option<u32>,
23 hovered_node: Option<u32>,
25 layout_iterations: u32,
27 highlight_patterns: Vec<GpuPatternMatch>,
29}
30
31impl Default for DfgCanvas {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl DfgCanvas {
38 pub fn new() -> Self {
40 Self {
41 camera: Camera::default(),
42 positions: HashMap::new(),
43 activity_names: HashMap::new(),
44 tokens: TokenAnimation::new(),
45 selected_node: None,
46 hovered_node: None,
47 layout_iterations: 0,
48 highlight_patterns: Vec::new(),
49 }
50 }
51
52 pub fn set_activity_names(&mut self, names: HashMap<u32, String>) {
54 self.activity_names = names;
55 }
56
57 pub fn set_highlight_patterns(&mut self, patterns: Vec<GpuPatternMatch>) {
59 self.highlight_patterns = patterns;
60 }
61
62 pub fn update_layout(&mut self, dfg: &DFGGraph) {
64 for node in dfg.nodes() {
66 if !self.positions.contains_key(&node.activity_id) {
67 let angle = self.positions.len() as f32 * 0.618 * std::f32::consts::TAU;
68 let radius = 100.0 + self.positions.len() as f32 * 30.0;
69 self.positions.insert(
70 node.activity_id,
71 NodePosition::new(angle.cos() * radius, angle.sin() * radius),
72 );
73 }
74 }
75
76 self.apply_forces(dfg);
78 }
79
80 fn apply_forces(&mut self, dfg: &DFGGraph) {
82 let nodes = dfg.nodes();
83 let edges = dfg.edges();
84
85 if nodes.is_empty() {
86 return;
87 }
88
89 let repulsion = 5000.0;
91 let attraction = 0.01;
92 let damping = 0.9;
93 let dt = 0.3;
94
95 let node_ids: Vec<u32> = nodes.iter().map(|n| n.activity_id).collect();
97
98 for i in 0..node_ids.len() {
100 for j in (i + 1)..node_ids.len() {
101 let id1 = node_ids[i];
102 let id2 = node_ids[j];
103
104 if let (Some(p1), Some(p2)) = (self.positions.get(&id1), self.positions.get(&id2)) {
105 let dx = p2.x - p1.x;
106 let dy = p2.y - p1.y;
107 let dist_sq = (dx * dx + dy * dy).max(100.0);
108 let force = repulsion / dist_sq;
109 let dist = dist_sq.sqrt();
110
111 let fx = force * dx / dist;
112 let fy = force * dy / dist;
113
114 if let Some(p) = self.positions.get_mut(&id1) {
115 p.vx -= fx;
116 p.vy -= fy;
117 }
118 if let Some(p) = self.positions.get_mut(&id2) {
119 p.vx += fx;
120 p.vy += fy;
121 }
122 }
123 }
124 }
125
126 for edge in edges {
128 if let (Some(p1), Some(p2)) = (
129 self.positions.get(&edge.source_activity),
130 self.positions.get(&edge.target_activity),
131 ) {
132 let dx = p2.x - p1.x;
133 let dy = p2.y - p1.y;
134 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
135
136 let target_dist = 150.0 - (edge.frequency as f32).log2().min(50.0);
138 let force = attraction * (dist - target_dist);
139
140 let fx = force * dx / dist;
141 let fy = force * dy / dist;
142
143 let src = edge.source_activity;
144 let tgt = edge.target_activity;
145
146 if let Some(p) = self.positions.get_mut(&src) {
147 p.vx += fx;
148 p.vy += fy;
149 }
150 if let Some(p) = self.positions.get_mut(&tgt) {
151 p.vx -= fx;
152 p.vy -= fy;
153 }
154 }
155 }
156
157 for pos in self.positions.values_mut() {
159 pos.vx *= damping;
160 pos.vy *= damping;
161 pos.x += pos.vx * dt;
162 pos.y += pos.vy * dt;
163 }
164
165 self.layout_iterations += 1;
166 }
167
168 pub fn render(&mut self, ui: &mut egui::Ui, theme: &Theme, dfg: &DFGGraph, rect: Rect) {
170 let painter = ui.painter_at(rect);
171 let center = rect.center();
172
173 self.handle_input(ui, rect);
175
176 self.camera.update(ui.ctx().input(|i| i.stable_dt));
178
179 self.tokens.update(ui.ctx().input(|i| i.stable_dt));
181
182 for edge in dfg.edges() {
184 self.draw_edge(&painter, theme, edge, center, dfg);
185 }
186
187 for token in self.tokens.active_tokens() {
189 if let (Some(src_pos), Some(tgt_pos)) = (
190 self.positions.get(&token.source_activity),
191 self.positions.get(&token.target_activity),
192 ) {
193 let p1 = self.camera.world_to_screen(src_pos.pos(), center);
194 let p2 = self.camera.world_to_screen(tgt_pos.pos(), center);
195 let pos = p1 + (p2 - p1) * token.progress;
196
197 painter.circle_filled(pos, 8.0, theme.token.linear_multiply(0.3));
199 painter.circle_filled(pos, 5.0, theme.token);
200 }
201 }
202
203 for node in dfg.nodes() {
205 self.draw_node(&painter, theme, node, center);
206 }
207
208 self.draw_pattern_highlights(&painter, theme, center);
210 }
211
212 fn handle_input(&mut self, ui: &mut egui::Ui, rect: Rect) {
214 let response = ui.interact(
215 rect,
216 ui.id().with("dfg_canvas"),
217 egui::Sense::click_and_drag(),
218 );
219
220 if response.dragged() {
222 self.camera.pan_by(response.drag_delta());
223 }
224
225 let scroll = ui.ctx().input(|i| i.raw_scroll_delta.y);
227 if scroll != 0.0 {
228 if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
229 if rect.contains(pointer) {
230 self.camera.zoom_by(scroll * 0.01, pointer, rect.center());
231 }
232 }
233 }
234
235 if response.double_clicked() {
237 self.camera.reset();
238 }
239
240 self.hovered_node = None;
242 if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
243 if rect.contains(pointer) {
244 let world_pos = self.camera.screen_to_world(pointer, rect.center());
245 for (id, pos) in &self.positions {
246 let dist = (Pos2::new(pos.x, pos.y) - world_pos).length();
247 if dist < 30.0 / self.camera.zoom {
248 self.hovered_node = Some(*id);
249 break;
250 }
251 }
252 }
253 }
254
255 if response.clicked() {
257 self.selected_node = self.hovered_node;
258 }
259 }
260
261 fn draw_node(&self, painter: &egui::Painter, theme: &Theme, node: &GpuDFGNode, center: Pos2) {
263 if let Some(pos) = self.positions.get(&node.activity_id) {
264 let screen_pos = self.camera.world_to_screen(pos.pos(), center);
265
266 let base_size = 20.0;
268 let size = base_size + (node.event_count as f32).log2().min(20.0) * 2.0;
269 let size = size * self.camera.zoom;
270
271 let color = if node.is_start != 0 {
273 theme.node_start
274 } else if node.is_end != 0 {
275 theme.node_end
276 } else {
277 theme.node_default
278 };
279
280 let is_highlighted = self.selected_node == Some(node.activity_id)
282 || self.hovered_node == Some(node.activity_id);
283
284 if is_highlighted {
285 painter.circle_filled(screen_pos, size + 4.0, color.linear_multiply(0.3));
286 }
287
288 painter.circle_filled(screen_pos, size, color);
290 painter.circle_stroke(
291 screen_pos,
292 size,
293 Stroke::new(2.0, Color32::WHITE.linear_multiply(0.3)),
294 );
295
296 let name = self
298 .activity_names
299 .get(&node.activity_id)
300 .cloned()
301 .unwrap_or_else(|| format!("A{}", node.activity_id));
302
303 let label_pos = screen_pos + Vec2::new(0.0, size + 12.0);
304 painter.text(
305 label_pos,
306 egui::Align2::CENTER_TOP,
307 &name,
308 egui::FontId::proportional(11.0 * self.camera.zoom.sqrt()),
309 theme.text,
310 );
311
312 if node.event_count > 0 {
314 let badge_pos = screen_pos + Vec2::new(size * 0.7, -size * 0.7);
315 painter.circle_filled(badge_pos, 10.0 * self.camera.zoom, theme.accent);
316 painter.text(
317 badge_pos,
318 egui::Align2::CENTER_CENTER,
319 format!("{}", node.event_count),
320 egui::FontId::proportional(8.0 * self.camera.zoom),
321 Color32::WHITE,
322 );
323 }
324 }
325 }
326
327 fn draw_edge(
329 &self,
330 painter: &egui::Painter,
331 theme: &Theme,
332 edge: &GpuDFGEdge,
333 center: Pos2,
334 dfg: &DFGGraph,
335 ) {
336 if let (Some(src_pos), Some(tgt_pos)) = (
337 self.positions.get(&edge.source_activity),
338 self.positions.get(&edge.target_activity),
339 ) {
340 let p1 = self.camera.world_to_screen(src_pos.pos(), center);
341 let p2 = self.camera.world_to_screen(tgt_pos.pos(), center);
342
343 let width = 1.0 + (edge.frequency as f32).log2().min(4.0);
345
346 let avg_duration = dfg.nodes().iter().map(|n| n.avg_duration_ms).sum::<f32>()
348 / dfg.nodes().len().max(1) as f32;
349 let color = theme.edge_duration_color(edge.avg_duration_ms, avg_duration);
350
351 painter.line_segment([p1, p2], Stroke::new(width, color.linear_multiply(0.7)));
353
354 let dir = (p2 - p1).normalized();
356 let arrow_size = 8.0 * self.camera.zoom;
357 let arrow_pos = p2 - dir * 25.0 * self.camera.zoom;
358
359 let perp = Vec2::new(-dir.y, dir.x);
360 let arrow_p1 = arrow_pos - dir * arrow_size + perp * arrow_size * 0.5;
361 let arrow_p2 = arrow_pos - dir * arrow_size - perp * arrow_size * 0.5;
362
363 painter.add(egui::Shape::convex_polygon(
364 vec![arrow_pos, arrow_p1, arrow_p2],
365 color,
366 Stroke::NONE,
367 ));
368
369 if edge.frequency > 1 {
371 let mid = p1 + (p2 - p1) * 0.5;
372 let label_offset = perp * 12.0;
373 painter.text(
374 mid + label_offset,
375 egui::Align2::CENTER_CENTER,
376 format!("{}", edge.frequency),
377 egui::FontId::proportional(9.0),
378 theme.text_muted,
379 );
380 }
381 }
382 }
383
384 fn draw_pattern_highlights(&self, painter: &egui::Painter, theme: &Theme, center: Pos2) {
386 for pattern in &self.highlight_patterns {
387 let pattern_type = pattern.get_pattern_type();
388 let color = match pattern_type {
389 PatternType::Bottleneck => theme.bottleneck,
390 PatternType::Loop | PatternType::Rework => theme.loop_highlight,
391 PatternType::LongRunning => theme.warning,
392 _ => theme.accent,
393 };
394
395 for &activity_id in pattern.activities() {
397 if let Some(pos) = self.positions.get(&activity_id) {
398 let screen_pos = self.camera.world_to_screen(pos.pos(), center);
399 let size = 35.0 * self.camera.zoom;
400
401 let time = painter.ctx().input(|i| i.time);
403 let alpha = ((time * 2.0).sin() * 0.3 + 0.5) as f32;
404
405 painter.circle_stroke(
406 screen_pos,
407 size,
408 Stroke::new(3.0, color.linear_multiply(alpha)),
409 );
410 painter.circle_stroke(
411 screen_pos,
412 size + 5.0,
413 Stroke::new(2.0, color.linear_multiply(alpha * 0.5)),
414 );
415 }
416 }
417 }
418 }
419
420 pub fn reset(&mut self) {
422 self.positions.clear();
423 self.layout_iterations = 0;
424 self.tokens.clear();
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_canvas_creation() {
434 let canvas = DfgCanvas::new();
435 assert!(canvas.positions.is_empty());
436 }
437}