1use super::bounds::calculate_bounds;
7use super::edge::render_edge;
8use super::node::render_node;
9use ratatui::{
10 buffer::Buffer,
11 layout::Rect,
12 style::Color,
13 widgets::{
14 canvas::{Canvas, Context},
15 Block, Widget,
16 },
17};
18use std::collections::HashMap;
19
20#[derive(Debug, Clone)]
25pub struct GraphNode {
26 pub id: String,
28 pub x: f64,
30 pub y: f64,
32 pub width: f64,
34 pub height: f64,
36 pub color: Color,
38 pub label: String,
40 pub selected: bool,
42}
43
44impl GraphNode {
45 pub fn new(id: impl Into<String>, x: f64, y: f64, label: impl Into<String>) -> Self {
58 Self {
59 id: id.into(),
60 x,
61 y,
62 width: 8.0,
63 height: 4.0,
64 color: Color::Blue,
65 label: label.into(),
66 selected: false,
67 }
68 }
69
70 pub fn builder(id: impl Into<String>) -> GraphNodeBuilder {
72 GraphNodeBuilder::new(id)
73 }
74}
75
76#[derive(Debug)]
78pub struct GraphNodeBuilder {
79 id: String,
80 x: f64,
81 y: f64,
82 width: f64,
83 height: f64,
84 color: Color,
85 label: String,
86 selected: bool,
87}
88
89impl GraphNodeBuilder {
90 pub fn new(id: impl Into<String>) -> Self {
92 let id = id.into();
93 Self {
94 label: id.clone(),
95 id,
96 x: 0.0,
97 y: 0.0,
98 width: 8.0,
99 height: 4.0,
100 color: Color::Blue,
101 selected: false,
102 }
103 }
104
105 pub fn position(mut self, x: f64, y: f64) -> Self {
107 self.x = x;
108 self.y = y;
109 self
110 }
111
112 pub fn size(mut self, width: f64, height: f64) -> Self {
114 self.width = width;
115 self.height = height;
116 self
117 }
118
119 pub fn color(mut self, color: Color) -> Self {
121 self.color = color;
122 self
123 }
124
125 pub fn label(mut self, label: impl Into<String>) -> Self {
127 self.label = label.into();
128 self
129 }
130
131 pub fn selected(mut self, selected: bool) -> Self {
133 self.selected = selected;
134 self
135 }
136
137 pub fn build(self) -> GraphNode {
139 GraphNode {
140 id: self.id,
141 x: self.x,
142 y: self.y,
143 width: self.width,
144 height: self.height,
145 color: self.color,
146 label: self.label,
147 selected: self.selected,
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
157pub struct GraphEdge {
158 pub from: String,
160 pub to: String,
162 pub color: Color,
164 pub label: Option<String>,
166}
167
168impl GraphEdge {
169 pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
180 Self {
181 from: from.into(),
182 to: to.into(),
183 color: Color::Cyan,
184 label: None,
185 }
186 }
187
188 pub fn color(mut self, color: Color) -> Self {
190 self.color = color;
191 self
192 }
193
194 pub fn label(mut self, label: impl Into<String>) -> Self {
196 self.label = Some(label.into());
197 self
198 }
199}
200
201#[derive(Debug, Clone, Default)]
205pub struct GraphData {
206 pub nodes: Vec<GraphNode>,
208 pub edges: Vec<GraphEdge>,
210}
211
212impl GraphData {
213 pub fn new() -> Self {
215 Self::default()
216 }
217
218 pub fn with_data(nodes: Vec<GraphNode>, edges: Vec<GraphEdge>) -> Self {
220 Self { nodes, edges }
221 }
222
223 pub fn add_node(&mut self, node: GraphNode) {
225 self.nodes.push(node);
226 }
227
228 pub fn add_edge(&mut self, edge: GraphEdge) {
230 self.edges.push(edge);
231 }
232
233 pub fn find_node(&self, id: &str) -> Option<&GraphNode> {
235 self.nodes.iter().find(|n| n.id == id)
236 }
237
238 pub fn find_node_mut(&mut self, id: &str) -> Option<&mut GraphNode> {
240 self.nodes.iter_mut().find(|n| n.id == id)
241 }
242
243 pub fn edges_from(&self, node_id: &str) -> Vec<&GraphEdge> {
245 self.edges.iter().filter(|e| e.from == node_id).collect()
246 }
247
248 pub fn edges_to(&self, node_id: &str) -> Vec<&GraphEdge> {
250 self.edges.iter().filter(|e| e.to == node_id).collect()
251 }
252}
253
254pub struct GraphCanvas<'a> {
259 graph: &'a GraphData,
260 block: Option<Block<'a>>,
261 x_bounds: Option<(f64, f64)>,
262 y_bounds: Option<(f64, f64)>,
263}
264
265impl<'a> GraphCanvas<'a> {
266 pub fn new(graph: &'a GraphData) -> Self {
286 Self {
287 graph,
288 block: None,
289 x_bounds: None,
290 y_bounds: None,
291 }
292 }
293
294 pub fn block(mut self, block: Block<'a>) -> Self {
296 self.block = Some(block);
297 self
298 }
299
300 pub fn x_bounds(mut self, min: f64, max: f64) -> Self {
302 self.x_bounds = Some((min, max));
303 self
304 }
305
306 pub fn y_bounds(mut self, min: f64, max: f64) -> Self {
308 self.y_bounds = Some((min, max));
309 self
310 }
311
312 fn get_bounds(&self) -> (f64, f64, f64, f64) {
314 let (auto_min_x, auto_max_x, auto_min_y, auto_max_y) = calculate_bounds(&self.graph.nodes);
315
316 let (min_x, max_x) = self.x_bounds.unwrap_or((auto_min_x, auto_max_x));
317 let (min_y, max_y) = self.y_bounds.unwrap_or((auto_min_y, auto_max_y));
318
319 (min_x, max_x, min_y, max_y)
320 }
321
322 #[allow(dead_code)]
324 fn paint(&self, ctx: &mut Context) {
325 let node_map: HashMap<String, &GraphNode> =
327 self.graph.nodes.iter().map(|n| (n.id.clone(), n)).collect();
328
329 for edge in &self.graph.edges {
331 render_edge(ctx, edge, &node_map);
332 }
333
334 for node in &self.graph.nodes {
336 render_node(ctx, node);
337 }
338 }
339}
340
341impl<'a> Widget for GraphCanvas<'a> {
342 fn render(self, area: Rect, buf: &mut Buffer) {
343 let (min_x, max_x, min_y, max_y) = self.get_bounds();
345
346 let nodes = self.graph.nodes.clone();
348 let edges = self.graph.edges.clone();
349
350 let canvas = Canvas::default()
352 .block(self.block.unwrap_or_default())
353 .x_bounds([min_x, max_x])
354 .y_bounds([min_y, max_y])
355 .paint(move |ctx| {
356 let node_map: HashMap<String, &GraphNode> =
358 nodes.iter().map(|n| (n.id.clone(), n)).collect();
359
360 for edge in &edges {
362 render_edge(ctx, edge, &node_map);
363 }
364
365 for node in &nodes {
367 render_node(ctx, node);
368 }
369 });
370
371 canvas.render(area, buf);
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_graph_node_new() {
382 let node = GraphNode::new("node1", 10.0, 20.0, "Test Node");
383 assert_eq!(node.id, "node1");
384 assert_eq!(node.x, 10.0);
385 assert_eq!(node.y, 20.0);
386 assert_eq!(node.label, "Test Node");
387 assert_eq!(node.width, 8.0);
388 assert_eq!(node.height, 4.0);
389 assert!(!node.selected);
390 }
391
392 #[test]
393 fn test_graph_node_builder() {
394 let node = GraphNode::builder("node1")
395 .position(10.0, 20.0)
396 .size(12.0, 6.0)
397 .color(Color::Red)
398 .label("Custom Node")
399 .selected(true)
400 .build();
401
402 assert_eq!(node.id, "node1");
403 assert_eq!(node.x, 10.0);
404 assert_eq!(node.y, 20.0);
405 assert_eq!(node.width, 12.0);
406 assert_eq!(node.height, 6.0);
407 assert_eq!(node.color, Color::Red);
408 assert_eq!(node.label, "Custom Node");
409 assert!(node.selected);
410 }
411
412 #[test]
413 fn test_graph_edge_new() {
414 let edge = GraphEdge::new("node1", "node2");
415 assert_eq!(edge.from, "node1");
416 assert_eq!(edge.to, "node2");
417 assert_eq!(edge.color, Color::Cyan);
418 assert!(edge.label.is_none());
419 }
420
421 #[test]
422 fn test_graph_edge_builder() {
423 let edge = GraphEdge::new("node1", "node2")
424 .color(Color::Green)
425 .label("100 ev/s");
426
427 assert_eq!(edge.color, Color::Green);
428 assert_eq!(edge.label, Some("100 ev/s".to_string()));
429 }
430
431 #[test]
432 fn test_graph_data_new() {
433 let graph = GraphData::new();
434 assert!(graph.nodes.is_empty());
435 assert!(graph.edges.is_empty());
436 }
437
438 #[test]
439 fn test_graph_data_add() {
440 let mut graph = GraphData::new();
441 graph.add_node(GraphNode::new("node1", 10.0, 20.0, "Node 1"));
442 graph.add_edge(GraphEdge::new("node1", "node2"));
443
444 assert_eq!(graph.nodes.len(), 1);
445 assert_eq!(graph.edges.len(), 1);
446 }
447
448 #[test]
449 fn test_graph_data_find_node() {
450 let mut graph = GraphData::new();
451 graph.add_node(GraphNode::new("node1", 10.0, 20.0, "Node 1"));
452
453 let found = graph.find_node("node1");
454 assert!(found.is_some());
455 assert_eq!(found.unwrap().id, "node1");
456
457 let not_found = graph.find_node("node2");
458 assert!(not_found.is_none());
459 }
460
461 #[test]
462 fn test_graph_data_edges_from() {
463 let mut graph = GraphData::new();
464 graph.add_edge(GraphEdge::new("node1", "node2"));
465 graph.add_edge(GraphEdge::new("node1", "node3"));
466 graph.add_edge(GraphEdge::new("node2", "node3"));
467
468 let edges = graph.edges_from("node1");
469 assert_eq!(edges.len(), 2);
470 }
471
472 #[test]
473 fn test_graph_data_edges_to() {
474 let mut graph = GraphData::new();
475 graph.add_edge(GraphEdge::new("node1", "node3"));
476 graph.add_edge(GraphEdge::new("node2", "node3"));
477 graph.add_edge(GraphEdge::new("node3", "node4"));
478
479 let edges = graph.edges_to("node3");
480 assert_eq!(edges.len(), 2);
481 }
482
483 #[test]
484 fn test_graph_canvas_new() {
485 let graph = GraphData::new();
486 let canvas = GraphCanvas::new(&graph);
487
488 let (min_x, max_x, min_y, max_y) = canvas.get_bounds();
489 assert_eq!(min_x, 0.0);
491 assert_eq!(max_x, 100.0);
492 assert_eq!(min_y, 0.0);
493 assert_eq!(max_y, 100.0);
494 }
495
496 #[test]
497 fn test_graph_canvas_custom_bounds() {
498 let graph = GraphData::new();
499 let canvas = GraphCanvas::new(&graph)
500 .x_bounds(-50.0, 50.0)
501 .y_bounds(-25.0, 25.0);
502
503 let (min_x, max_x, min_y, max_y) = canvas.get_bounds();
504 assert_eq!(min_x, -50.0);
505 assert_eq!(max_x, 50.0);
506 assert_eq!(min_y, -25.0);
507 assert_eq!(max_y, 25.0);
508 }
509}