1mod paint;
2mod visual;
3
4use std::{
5 cell::{Cell, RefCell},
6 collections::{HashMap, HashSet},
7 rc::Rc,
8};
9
10use std::time::{Duration, Instant};
11
12use gpui::{
13 AnyElement, App, Bounds, ContentMask, Context, CursorStyle, DispatchPhase, Entity,
14 EventEmitter, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement as _,
15 IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
16 Pixels, Point, Render, ScrollWheelEvent, SharedString, Styled as _, Window, canvas, div, point,
17 px,
18};
19
20use crate::actions::{
21 Activate, ClearSelection, FitView, KEY_CONTEXT, OpenContextMenu, SelectDown, SelectLeft,
22 SelectRight, SelectUp, ZoomIn, ZoomOut,
23};
24
25use crate::geometry::EdgeGeometry;
26use crate::layout::{Body, ForceConfig, Simulation, Vec2};
27use crate::model::{Edge, GraphData, Node};
28use crate::style::{ColorBy, GraphColors, GraphMetrics, LabelMode, NodeSizing, Palette};
29use visual::{GroupInput, Vertex, Visual};
30
31const MIN_ZOOM: f32 = 0.05;
32const MAX_ZOOM: f32 = 6.;
33const DRAG_THRESHOLD: f32 = 3.;
35const EDGE_HIT_SLOP: f32 = 4.;
37const REDUCED_MOTION_BUDGET: Duration = Duration::from_millis(120);
40
41#[derive(Clone, Debug, Default)]
43#[non_exhaustive]
44pub enum Selection {
45 #[default]
46 None,
47 Node(Node),
48 Edge(Edge),
49 EdgeBundle(Vec<Edge>),
51 Group(GroupInfo),
53}
54
55#[derive(Clone, Debug)]
56#[non_exhaustive]
57pub enum NetworkGraphEvent {
58 SelectionChanged(Selection),
59 NodeActivated(Node),
61 GroupToggled {
63 group: SharedString,
64 collapsed: bool,
65 },
66 ContextMenu {
68 target: Selection,
69 position: Point<Pixels>,
70 },
71}
72
73#[derive(Clone, Debug)]
75#[non_exhaustive]
76pub struct CategoryInfo {
77 pub name: SharedString,
78 pub color: Hsla,
79 pub count: usize,
80}
81
82#[derive(Clone, Debug)]
84#[non_exhaustive]
85pub struct GroupInfo {
86 pub id: SharedString,
87 pub label: SharedString,
88 pub color: Hsla,
89 pub size: usize,
91 pub collapsed: bool,
92}
93
94pub type TooltipBuilder =
96 Rc<dyn Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>>;
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum Item {
101 Vertex(usize),
102 Edge(usize),
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107enum SelectionKey {
108 Node(SharedString),
109 Group(SharedString),
110 Edges(Vec<usize>),
112}
113
114struct ModelEdge {
115 data: Edge,
116 source: usize,
117 target: usize,
118}
119
120struct GroupState {
121 id: SharedString,
122 label: SharedString,
123 members: Vec<usize>,
124 collapsed: bool,
125 body: Body,
127 anchor: Vec2,
129}
130
131enum DragKind {
132 Pan { start_pan: Point<Pixels> },
133 Vertex { ix: usize, was_pinned: bool },
134}
135
136struct Drag {
137 kind: DragKind,
138 start: Point<Pixels>,
139 moved: bool,
140}
141
142pub struct NetworkGraph {
155 nodes: Vec<Node>,
157 node_bodies: Vec<Body>,
158 edges: Vec<ModelEdge>,
159 index: HashMap<SharedString, usize>,
160 degrees: Vec<usize>,
161 neighbors: Vec<Vec<usize>>,
162 categories: Vec<SharedString>,
163 groups: Vec<GroupState>,
164 group_index: HashMap<SharedString, usize>,
165 node_group: Vec<Option<usize>>,
166
167 vis: Visual,
169 sim: Simulation,
170
171 pan: Point<Pixels>,
173 zoom: f32,
174 auto_fit: bool,
176 bounds: Rc<Cell<Bounds<Pixels>>>,
177
178 hovered: Option<Item>,
179 selected: Option<SelectionKey>,
180 drag: Option<Drag>,
181 pointer: Option<Point<Pixels>>,
182 focus_handle: FocusHandle,
183 keyboard_nav: bool,
185 shown_labels: RefCell<HashSet<usize>>,
187
188 label_mode: LabelMode,
189 color_by: ColorBy,
190 arrows: bool,
191 edge_labels: bool,
192 group_hulls: bool,
193 sticky_drag: bool,
195 palette: Palette,
196 sizing: NodeSizing,
197 colors: Option<GraphColors>,
198 tooltip: Option<TooltipBuilder>,
199 tooltips: bool,
200}
201
202impl EventEmitter<NetworkGraphEvent> for NetworkGraph {}
203
204impl Focusable for NetworkGraph {
205 fn focus_handle(&self, _: &App) -> FocusHandle {
206 self.focus_handle.clone()
207 }
208}
209
210impl NetworkGraph {
211 pub fn new(cx: &mut Context<Self>) -> Self {
212 Self {
213 nodes: Vec::new(),
214 node_bodies: Vec::new(),
215 edges: Vec::new(),
216 index: HashMap::new(),
217 degrees: Vec::new(),
218 neighbors: Vec::new(),
219 categories: Vec::new(),
220 groups: Vec::new(),
221 group_index: HashMap::new(),
222 node_group: Vec::new(),
223 vis: Visual::default(),
224 sim: Simulation::new(ForceConfig::default()),
225 pan: Point::default(),
226 zoom: 1.,
227 auto_fit: true,
228 bounds: Rc::new(Cell::new(Bounds::default())),
229 hovered: None,
230 selected: None,
231 drag: None,
232 pointer: None,
233 focus_handle: cx.focus_handle().tab_stop(true),
234 keyboard_nav: false,
235 shown_labels: RefCell::default(),
236 label_mode: LabelMode::default(),
237 color_by: ColorBy::default(),
238 arrows: true,
239 edge_labels: true,
240 group_hulls: true,
241 sticky_drag: false,
242 palette: Palette::default(),
243 sizing: NodeSizing::default(),
244 colors: None,
245 tooltip: None,
246 tooltips: true,
247 }
248 }
249
250 pub fn with_data(mut self, data: GraphData) -> Self {
253 self.load(data);
254 self
255 }
256
257 pub fn with_force_config(mut self, config: ForceConfig) -> Self {
258 self.sim.config = config;
259 self
260 }
261
262 pub fn with_label_mode(mut self, mode: LabelMode) -> Self {
263 self.label_mode = mode;
264 self
265 }
266
267 pub fn with_color_by(mut self, color_by: ColorBy) -> Self {
268 self.color_by = color_by;
269 self
270 }
271
272 pub fn arrows(mut self, arrows: bool) -> Self {
274 self.arrows = arrows;
275 self
276 }
277
278 pub fn edge_labels(mut self, edge_labels: bool) -> Self {
280 self.edge_labels = edge_labels;
281 self
282 }
283
284 pub fn group_hulls(mut self, group_hulls: bool) -> Self {
286 self.group_hulls = group_hulls;
287 self
288 }
289
290 pub fn sticky_drag(mut self, sticky: bool) -> Self {
292 self.sticky_drag = sticky;
293 self
294 }
295
296 pub fn with_palette(mut self, palette: Palette) -> Self {
297 self.palette = palette;
298 self
299 }
300
301 pub fn with_node_sizing(mut self, sizing: NodeSizing) -> Self {
302 self.sizing = sizing;
303 self
304 }
305
306 pub fn with_colors(mut self, colors: GraphColors) -> Self {
308 self.colors = Some(colors);
309 self
310 }
311
312 pub fn with_tooltip(
314 mut self,
315 builder: impl Fn(&Selection, &GraphColors, &mut Window, &mut App) -> Option<AnyElement>
316 + 'static,
317 ) -> Self {
318 self.tooltip = Some(Rc::new(builder));
319 self
320 }
321
322 pub fn tooltips(mut self, tooltips: bool) -> Self {
324 self.tooltips = tooltips;
325 self
326 }
327
328 pub fn label_mode(&self) -> LabelMode {
331 self.label_mode
332 }
333
334 pub fn set_label_mode(&mut self, mode: LabelMode, cx: &mut Context<Self>) {
335 self.label_mode = mode;
336 cx.notify();
337 }
338
339 pub fn color_by(&self) -> ColorBy {
340 self.color_by
341 }
342
343 pub fn set_color_by(&mut self, color_by: ColorBy, cx: &mut Context<Self>) {
344 self.color_by = color_by;
345 cx.notify();
346 }
347
348 pub fn has_arrows(&self) -> bool {
349 self.arrows
350 }
351
352 pub fn set_arrows(&mut self, arrows: bool, cx: &mut Context<Self>) {
353 self.arrows = arrows;
354 cx.notify();
355 }
356
357 pub fn has_edge_labels(&self) -> bool {
358 self.edge_labels
359 }
360
361 pub fn set_edge_labels(&mut self, edge_labels: bool, cx: &mut Context<Self>) {
362 self.edge_labels = edge_labels;
363 cx.notify();
364 }
365
366 pub fn has_group_hulls(&self) -> bool {
367 self.group_hulls
368 }
369
370 pub fn set_group_hulls(&mut self, group_hulls: bool, cx: &mut Context<Self>) {
371 self.group_hulls = group_hulls;
372 cx.notify();
373 }
374
375 pub fn is_sticky_drag(&self) -> bool {
376 self.sticky_drag
377 }
378
379 pub fn set_sticky_drag(&mut self, sticky: bool) {
380 self.sticky_drag = sticky;
381 }
382
383 pub fn has_tooltips(&self) -> bool {
384 self.tooltips
385 }
386
387 pub fn set_tooltips(&mut self, tooltips: bool, cx: &mut Context<Self>) {
388 self.tooltips = tooltips;
389 cx.notify();
390 }
391
392 pub fn set_force_config(&mut self, config: ForceConfig, cx: &mut Context<Self>) {
393 self.sim.config = config;
394 self.sim.reheat(0.5);
395 cx.notify();
396 }
397
398 pub fn set_colors(&mut self, colors: Option<GraphColors>, cx: &mut Context<Self>) {
399 self.colors = colors;
400 cx.notify();
401 }
402
403 pub fn set_data(&mut self, data: GraphData, cx: &mut Context<Self>) {
407 let had_selection = self.selected.is_some();
408 self.load(data);
409 if had_selection {
410 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
411 }
412 cx.notify();
413 }
414
415 pub fn merge(&mut self, data: GraphData, anchor: Option<&str>, cx: &mut Context<Self>) {
419 self.sync_back();
420 self.insert(data, anchor);
421 cx.notify();
422 }
423
424 pub fn remove_nodes(&mut self, ids: &[&str], cx: &mut Context<Self>) {
426 let doomed: Vec<bool> = self
427 .nodes
428 .iter()
429 .map(|n| ids.contains(&n.id.as_ref()))
430 .collect();
431 if !doomed.contains(&true) {
432 return;
433 }
434 self.sync_back();
435 let mut remap = vec![usize::MAX; self.nodes.len()];
436 let mut kept = 0;
437 for (i, &d) in doomed.iter().enumerate() {
438 if !d {
439 remap[i] = kept;
440 kept += 1;
441 }
442 }
443 let mut i = 0;
444 self.nodes.retain(|_| (!doomed[i], i += 1).0);
445 let mut i = 0;
446 self.node_bodies.retain(|_| (!doomed[i], i += 1).0);
447 self.edges.retain_mut(|e| {
448 let keep = !doomed[e.source] && !doomed[e.target];
449 e.source = remap[e.source];
450 e.target = remap[e.target];
451 keep
452 });
453 self.index = self
454 .nodes
455 .iter()
456 .enumerate()
457 .map(|(i, n)| (n.id.clone(), i))
458 .collect();
459 self.categories
460 .retain(|c| self.nodes.iter().any(|n| &n.category == c));
461 if matches!(self.selected, Some(SelectionKey::Edges(_))) {
463 self.selected = None;
464 }
465 self.rebuild();
466 self.sim.reheat(0.3);
467 self.validate_selection(cx);
468 cx.notify();
469 }
470
471 pub fn clear(&mut self, cx: &mut Context<Self>) {
472 self.set_data(GraphData::default(), cx);
473 }
474
475 pub fn data(&self) -> GraphData {
477 GraphData {
478 nodes: self.nodes.clone(),
479 edges: self.edges.iter().map(|e| e.data.clone()).collect(),
480 }
481 }
482
483 fn load(&mut self, data: GraphData) {
484 self.nodes.clear();
485 self.node_bodies.clear();
486 self.edges.clear();
487 self.index.clear();
488 self.categories.clear();
489 self.groups.clear();
490 self.group_index.clear();
491 self.vis = Visual::default();
492 self.hovered = None;
493 self.selected = None;
494 self.drag = None;
495 self.insert(data, None);
496 self.pan = Point::default();
497 self.zoom = 1.;
498 self.auto_fit = true;
499 }
500
501 fn insert(&mut self, data: GraphData, anchor: Option<&str>) {
503 let anchor_pos = anchor
504 .and_then(|id| self.index.get(id))
505 .map(|&i| self.vis.bodies[self.vis.node_slot[i]].pos);
506 let first_new = self.nodes.len();
507 for node in data.nodes {
508 if self.index.contains_key(&node.id) {
509 continue;
510 }
511 if !self.categories.contains(&node.category) {
512 self.categories.push(node.category.clone());
513 }
514 self.index.insert(node.id.clone(), self.nodes.len());
515 self.nodes.push(node);
516 self.node_bodies.push(Body::default());
517 }
518 match anchor_pos {
519 Some(center) => {
520 let added = self.nodes.len() - first_new;
521 for (k, body) in self.node_bodies[first_new..].iter_mut().enumerate() {
522 let a = k as f32 / added.max(1) as f32 * std::f32::consts::TAU;
523 body.pos = Vec2::new(center.x + 30. * a.cos(), center.y + 30. * a.sin());
524 }
525 }
526 None => Simulation::initialize_positions(&mut self.node_bodies, first_new),
527 }
528
529 for edge in data.edges {
530 let (Some(&s), Some(&t)) = (self.index.get(&edge.source), self.index.get(&edge.target))
531 else {
532 continue;
533 };
534 let duplicate = self
535 .edges
536 .iter()
537 .any(|e| e.source == s && e.target == t && e.data.label == edge.label);
538 if !duplicate {
539 self.edges.push(ModelEdge {
540 data: edge,
541 source: s,
542 target: t,
543 });
544 }
545 }
546 self.rebuild();
547 self.sim.reheat(if anchor_pos.is_some() { 0.5 } else { 1. });
548 }
549
550 fn sync_back(&mut self) {
552 for (v, vertex) in self.vis.vertices.iter().enumerate() {
553 let body = self.vis.bodies[v].clone();
554 match *vertex {
555 Vertex::Node(i) => self.node_bodies[i] = body,
556 Vertex::Group(g) => self.groups[g].body = body,
557 }
558 }
559 }
560
561 fn rebuild(&mut self) {
563 let n = self.nodes.len();
564 self.degrees = vec![0; n];
565 self.neighbors = vec![Vec::new(); n];
566 for e in &self.edges {
567 self.degrees[e.source] += 1;
568 self.degrees[e.target] += 1;
569 if e.source != e.target {
570 self.neighbors[e.source].push(e.target);
571 self.neighbors[e.target].push(e.source);
572 }
573 }
574 for list in &mut self.neighbors {
575 list.sort_unstable();
576 list.dedup();
577 }
578 for (i, body) in self.node_bodies.iter_mut().enumerate() {
579 body.radius = self.nodes[i]
580 .size
581 .unwrap_or_else(|| self.sizing.radius(self.degrees[i]));
582 }
583
584 for g in &mut self.groups {
586 g.members.clear();
587 }
588 self.node_group = vec![None; n];
589 for (i, node) in self.nodes.iter().enumerate() {
590 let Some(id) = &node.group else { continue };
591 let g = *self.group_index.entry(id.clone()).or_insert_with(|| {
592 self.groups.push(GroupState {
593 id: id.clone(),
594 label: id.clone(),
595 members: Vec::new(),
596 collapsed: false,
597 body: Body::default(),
598 anchor: Vec2::default(),
599 });
600 self.groups.len() - 1
601 });
602 self.groups[g].members.push(i);
603 self.node_group[i] = Some(g);
604 }
605 if self.groups.iter().any(|g| g.members.is_empty()) {
606 self.groups.retain(|g| !g.members.is_empty());
607 self.group_index = self
608 .groups
609 .iter()
610 .enumerate()
611 .map(|(g, s)| (s.id.clone(), g))
612 .collect();
613 for (i, node) in self.nodes.iter().enumerate() {
614 self.node_group[i] = node.group.as_ref().map(|id| self.group_index[id]);
615 }
616 }
617 for g in &mut self.groups {
618 g.body.radius = (12. + (g.members.len() as f32).sqrt() * 3.).min(48.);
619 }
620
621 let groups: Vec<GroupInput> = self
622 .groups
623 .iter()
624 .map(|g| GroupInput {
625 collapsed: g.collapsed,
626 body: &g.body,
627 })
628 .collect();
629 let links: Vec<(usize, usize)> = self.edges.iter().map(|e| (e.source, e.target)).collect();
630 self.vis = Visual::build(&self.node_bodies, &self.node_group, &groups, &links);
631 self.hovered = None;
632 self.drag = None;
633 self.shown_labels.take();
635 }
636
637 pub fn node_count(&self) -> usize {
640 self.nodes.len()
641 }
642
643 pub fn edge_count(&self) -> usize {
644 self.edges.len()
645 }
646
647 pub fn node(&self, id: &str) -> Option<&Node> {
648 self.index.get(id).map(|&i| &self.nodes[i])
649 }
650
651 pub fn degree(&self, id: &str) -> usize {
652 self.index.get(id).map_or(0, |&i| self.degrees[i])
653 }
654
655 pub fn neighbors_of(&self, id: &str) -> Vec<Node> {
656 self.index
657 .get(id)
658 .map(|&i| {
659 self.neighbors[i]
660 .iter()
661 .map(|&j| self.nodes[j].clone())
662 .collect()
663 })
664 .unwrap_or_default()
665 }
666
667 pub fn edges_of(&self, id: &str) -> Vec<Edge> {
669 let Some(&i) = self.index.get(id) else {
670 return Vec::new();
671 };
672 self.edges
673 .iter()
674 .filter(|e| e.source == i || e.target == i)
675 .map(|e| e.data.clone())
676 .collect()
677 }
678
679 pub fn categories(&self) -> Vec<CategoryInfo> {
680 self.categories
681 .iter()
682 .enumerate()
683 .map(|(i, c)| CategoryInfo {
684 name: c.clone(),
685 color: self.palette.get(i),
686 count: self.nodes.iter().filter(|n| &n.category == c).count(),
687 })
688 .collect()
689 }
690
691 pub fn groups(&self) -> Vec<GroupInfo> {
692 (0..self.groups.len()).map(|g| self.group_info(g)).collect()
693 }
694
695 pub fn group(&self, id: &str) -> Option<GroupInfo> {
696 self.group_index.get(id).map(|&g| self.group_info(g))
697 }
698
699 pub fn group_members(&self, id: &str) -> Vec<Node> {
701 self.group_index
702 .get(id)
703 .map(|&g| {
704 self.groups[g]
705 .members
706 .iter()
707 .map(|&i| self.nodes[i].clone())
708 .collect()
709 })
710 .unwrap_or_default()
711 }
712
713 pub fn resolved_colors(&self, cx: &App) -> GraphColors {
715 self.colors
716 .clone()
717 .unwrap_or_else(|| GraphColors::from_theme(cx))
718 }
719
720 pub fn color_of(&self, node: &Node, cx: &App) -> Hsla {
722 if let Some(c) = node.color {
723 return c;
724 }
725 match self.color_by {
726 ColorBy::Category => {
727 let ix = self
728 .categories
729 .iter()
730 .position(|c| c == &node.category)
731 .unwrap_or(0);
732 self.palette.get(ix)
733 }
734 ColorBy::Group => match node.group.as_ref().and_then(|g| self.group_index.get(g)) {
735 Some(&g) => self.group_color(g),
736 None => self.resolved_colors(cx).ungrouped,
737 },
738 }
739 }
740
741 pub fn selection(&self) -> Selection {
742 match &self.selected {
743 None => Selection::None,
744 Some(SelectionKey::Node(id)) => self
745 .node(id)
746 .cloned()
747 .map_or(Selection::None, Selection::Node),
748 Some(SelectionKey::Group(id)) => {
749 self.group(id).map_or(Selection::None, Selection::Group)
750 }
751 Some(SelectionKey::Edges(edges)) => self.edge_selection(edges),
752 }
753 }
754
755 pub fn zoom(&self) -> f32 {
756 self.zoom
757 }
758
759 pub fn is_simulating(&self) -> bool {
761 self.sim.is_active()
762 }
763
764 fn group_info(&self, g: usize) -> GroupInfo {
765 let s = &self.groups[g];
766 GroupInfo {
767 id: s.id.clone(),
768 label: s.label.clone(),
769 color: self.group_color(g),
770 size: s.members.len(),
771 collapsed: s.collapsed,
772 }
773 }
774
775 fn group_color(&self, g: usize) -> Hsla {
776 self.palette.get(g)
777 }
778
779 fn edge_selection(&self, edges: &[usize]) -> Selection {
780 match edges {
781 [] => Selection::None,
782 [e] => Selection::Edge(self.edges[*e].data.clone()),
783 many => {
784 Selection::EdgeBundle(many.iter().map(|&e| self.edges[e].data.clone()).collect())
785 }
786 }
787 }
788
789 fn key_of(&self, item: Item) -> SelectionKey {
790 match item {
791 Item::Vertex(v) => match self.vis.vertices[v] {
792 Vertex::Node(i) => SelectionKey::Node(self.nodes[i].id.clone()),
793 Vertex::Group(g) => SelectionKey::Group(self.groups[g].id.clone()),
794 },
795 Item::Edge(e) => SelectionKey::Edges(self.vis.edges[e].edges.clone()),
796 }
797 }
798
799 fn item_selection(&self, item: Item) -> Selection {
800 match item {
801 Item::Vertex(v) => match self.vis.vertices[v] {
802 Vertex::Node(i) => Selection::Node(self.nodes[i].clone()),
803 Vertex::Group(g) => Selection::Group(self.group_info(g)),
804 },
805 Item::Edge(e) => self.edge_selection(&self.vis.edges[e].edges),
806 }
807 }
808
809 fn selected_item(&self) -> Option<Item> {
811 match self.selected.as_ref()? {
812 SelectionKey::Node(id) => {
813 let slot = self.vis.node_slot[*self.index.get(id)?];
814 matches!(self.vis.vertices[slot], Vertex::Node(_)).then_some(Item::Vertex(slot))
815 }
816 SelectionKey::Group(id) => {
817 self.vis.group_slot[*self.group_index.get(id)?].map(Item::Vertex)
818 }
819 SelectionKey::Edges(edges) => self
820 .vis
821 .edges
822 .iter()
823 .position(|e| &e.edges == edges)
824 .map(Item::Edge),
825 }
826 }
827
828 fn validate_selection(&mut self, cx: &mut Context<Self>) {
830 if self.selected.is_some() && matches!(self.selection(), Selection::None) {
831 self.selected = None;
832 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
833 }
834 }
835
836 pub fn select_node(&mut self, id: Option<&str>, center: bool, cx: &mut Context<Self>) {
840 let key = id
841 .filter(|id| self.index.contains_key(*id))
842 .map(|id| SelectionKey::Node(id.to_string().into()));
843 if let (Some(id), true) = (id, center)
844 && let Some(&i) = self.index.get(id)
845 {
846 let p = self.vis.bodies[self.vis.node_slot[i]].pos;
847 self.auto_fit = false;
848 self.pan = point(px(-p.x * self.zoom), px(-p.y * self.zoom));
849 cx.notify();
850 }
851 self.set_selected(key, cx);
852 }
853
854 fn set_selected(&mut self, key: Option<SelectionKey>, cx: &mut Context<Self>) {
855 if key == self.selected {
856 return;
857 }
858 self.selected = key;
859 cx.emit(NetworkGraphEvent::SelectionChanged(self.selection()));
860 cx.notify();
861 }
862
863 pub fn set_pinned(&mut self, id: &str, pinned: bool, cx: &mut Context<Self>) {
865 let Some(&i) = self.index.get(id) else { return };
866 let v = self.vis.node_slot[i];
867 if matches!(self.vis.vertices[v], Vertex::Node(_)) {
868 let body = &mut self.vis.bodies[v];
869 body.fixed = pinned.then_some(body.pos);
870 } else {
871 let body = &mut self.node_bodies[i];
872 body.fixed = pinned.then_some(body.pos);
873 }
874 self.sim.reheat(0.1);
875 cx.notify();
876 }
877
878 pub fn is_pinned(&self, id: &str) -> bool {
879 self.index.get(id).is_some_and(|&i| {
880 let v = self.vis.node_slot[i];
881 match self.vis.vertices[v] {
882 Vertex::Node(_) => self.vis.bodies[v].fixed.is_some(),
883 Vertex::Group(_) => self.node_bodies[i].fixed.is_some(),
884 }
885 })
886 }
887
888 pub fn unpin_all(&mut self, cx: &mut Context<Self>) {
889 for b in self.vis.bodies.iter_mut().chain(&mut self.node_bodies) {
890 b.fixed = None;
891 }
892 self.sim.reheat(0.3);
893 cx.notify();
894 }
895
896 pub fn collapse_group(&mut self, id: &str, cx: &mut Context<Self>) {
900 self.set_groups_collapsed(|g| g.id.as_ref() == id, true, cx);
901 }
902
903 pub fn expand_group(&mut self, id: &str, cx: &mut Context<Self>) {
904 self.set_groups_collapsed(|g| g.id.as_ref() == id, false, cx);
905 }
906
907 pub fn toggle_group(&mut self, id: &str, cx: &mut Context<Self>) {
908 if let Some(&g) = self.group_index.get(id) {
909 let collapse = !self.groups[g].collapsed;
910 self.set_groups_collapsed(|s| s.id.as_ref() == id, collapse, cx);
911 }
912 }
913
914 pub fn collapse_all_groups(&mut self, cx: &mut Context<Self>) {
916 self.set_groups_collapsed(|_| true, true, cx);
917 }
918
919 pub fn expand_all_groups(&mut self, cx: &mut Context<Self>) {
920 self.set_groups_collapsed(|_| true, false, cx);
921 }
922
923 pub fn set_group_label(
924 &mut self,
925 id: &str,
926 label: impl Into<SharedString>,
927 cx: &mut Context<Self>,
928 ) {
929 if let Some(&g) = self.group_index.get(id) {
930 self.groups[g].label = label.into();
931 cx.notify();
932 }
933 }
934
935 fn set_groups_collapsed(
936 &mut self,
937 filter: impl Fn(&GroupState) -> bool,
938 collapse: bool,
939 cx: &mut Context<Self>,
940 ) {
941 let targets: Vec<usize> = (0..self.groups.len())
942 .filter(|&g| filter(&self.groups[g]) && self.groups[g].collapsed != collapse)
943 .collect();
944 if targets.is_empty() {
945 return;
946 }
947 self.sync_back();
948 for &g in &targets {
949 let members = self.groups[g].members.clone();
950 if collapse {
951 let n = members.len().max(1) as f32;
952 let (sx, sy) = members.iter().fold((0., 0.), |(x, y), &i| {
953 (x + self.node_bodies[i].pos.x, y + self.node_bodies[i].pos.y)
954 });
955 let centroid = Vec2::new(sx / n, sy / n);
956 let group = &mut self.groups[g];
957 group.anchor = centroid;
958 group.body.pos = centroid;
959 group.body.vel = Vec2::default();
960 group.body.fixed = None;
961 } else {
962 let group = &self.groups[g];
963 let (dx, dy) = (
964 group.body.pos.x - group.anchor.x,
965 group.body.pos.y - group.anchor.y,
966 );
967 for i in members {
968 let b = &mut self.node_bodies[i];
969 b.pos = Vec2::new(b.pos.x + dx, b.pos.y + dy);
970 b.vel = Vec2::default();
971 if let Some(f) = b.fixed {
972 b.fixed = Some(Vec2::new(f.x + dx, f.y + dy));
973 }
974 }
975 }
976 self.groups[g].collapsed = collapse;
977 }
978 if let (false, Some(SelectionKey::Group(id))) = (collapse, &self.selected)
980 && targets.iter().any(|&g| &self.groups[g].id == id)
981 {
982 self.selected = None;
983 cx.emit(NetworkGraphEvent::SelectionChanged(Selection::None));
984 }
985 self.rebuild();
986 self.sim.reheat(0.4);
987 for g in targets {
988 cx.emit(NetworkGraphEvent::GroupToggled {
989 group: self.groups[g].id.clone(),
990 collapsed: collapse,
991 });
992 }
993 cx.notify();
994 }
995
996 pub fn fit_view(&mut self, cx: &mut Context<Self>) {
1000 self.auto_fit = true;
1001 self.apply_fit(1.);
1002 cx.notify();
1003 }
1004
1005 pub fn zoom_by(&mut self, factor: f32, cx: &mut Context<Self>) {
1006 self.auto_fit = false;
1007 let center = self.bounds.get().center();
1008 self.zoom_around(center, factor);
1009 cx.notify();
1010 }
1011
1012 pub fn relayout(&mut self, cx: &mut Context<Self>) {
1014 for b in &mut self.vis.bodies {
1015 b.fixed = None;
1016 }
1017 Simulation::initialize_positions(&mut self.vis.bodies, 0);
1018 self.sim.reheat(1.);
1019 self.auto_fit = true;
1020 cx.notify();
1021 }
1022
1023 fn apply_fit(&mut self, blend: f32) {
1024 let bounds = self.bounds.get();
1025 if self.vis.bodies.is_empty() || bounds.size.width <= px(0.) {
1026 return;
1027 }
1028 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
1029 for b in &self.vis.bodies {
1030 x0 = x0.min(b.pos.x - b.radius);
1031 y0 = y0.min(b.pos.y - b.radius);
1032 x1 = x1.max(b.pos.x + b.radius);
1033 y1 = y1.max(b.pos.y + b.radius);
1034 }
1035 let padding = 48.;
1036 let w = (bounds.size.width.as_f32() - padding * 2.).max(1.);
1037 let h = (bounds.size.height.as_f32() - padding * 2.).max(1.);
1038 let zoom = (w / (x1 - x0).max(1.))
1039 .min(h / (y1 - y0).max(1.))
1040 .clamp(MIN_ZOOM, 2.);
1041 let (cx, cy) = ((x0 + x1) / 2., (y0 + y1) / 2.);
1042 let target = point(px(-cx * zoom), px(-cy * zoom));
1043 self.zoom += (zoom - self.zoom) * blend;
1044 self.pan = self.pan + (target - self.pan) * blend;
1045 }
1046
1047 fn zoom_around(&mut self, anchor: Point<Pixels>, factor: f32) {
1048 let world = self.to_world(anchor);
1049 self.zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM);
1050 let center = self.bounds.get().center();
1051 self.pan = anchor - center - point(px(world.x * self.zoom), px(world.y * self.zoom));
1052 }
1053
1054 fn to_screen(&self, p: Vec2) -> Point<Pixels> {
1055 self.bounds.get().center() + self.pan + point(px(p.x * self.zoom), px(p.y * self.zoom))
1056 }
1057
1058 fn to_world(&self, p: Point<Pixels>) -> Vec2 {
1059 let local = p - self.bounds.get().center() - self.pan;
1060 Vec2::new(local.x.as_f32() / self.zoom, local.y.as_f32() / self.zoom)
1061 }
1062
1063 fn edge_geometry(&self, e: usize) -> EdgeGeometry {
1064 let e = &self.vis.edges[e];
1065 let a = self.to_screen(self.vis.bodies[e.source].pos);
1066 let b = self.to_screen(self.vis.bodies[e.target].pos);
1067 EdgeGeometry::new(a, b, e.curvature * 28. * self.zoom.min(1.5))
1068 }
1069
1070 fn hit_test(&self, p: Point<Pixels>) -> Option<Item> {
1071 let w = self.to_world(p);
1072 let slop = 3. / self.zoom;
1073 let vertex = self.vis.bodies.iter().enumerate().rev().find_map(|(i, b)| {
1075 let (dx, dy) = (b.pos.x - w.x, b.pos.y - w.y);
1076 let r = b.radius + slop;
1077 (dx * dx + dy * dy <= r * r).then_some(i)
1078 });
1079 if let Some(v) = vertex {
1080 return Some(Item::Vertex(v));
1081 }
1082 let mut best = (EDGE_HIT_SLOP, None);
1083 for (i, e) in self.vis.edges.iter().enumerate() {
1084 let d = if e.source == e.target {
1085 let c = self.to_screen(self.vis.bodies[e.source].pos);
1086 let s = (self.vis.bodies[e.source].radius * self.zoom).max(6.);
1087 let center = point(c.x + px(s * 1.3), c.y - px(s * 1.3));
1088 let r = (p.x - center.x).as_f32().hypot((p.y - center.y).as_f32());
1089 (r - s * 0.9).abs()
1090 } else {
1091 self.edge_geometry(i).distance_to(p)
1092 };
1093 if d < best.0 {
1094 best = (d, Some(i));
1095 }
1096 }
1097 best.1.map(Item::Edge)
1098 }
1099
1100 fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
1103 self.keyboard_nav = true;
1104 self.zoom_by(1.25, cx);
1105 }
1106
1107 fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
1108 self.keyboard_nav = true;
1109 self.zoom_by(0.8, cx);
1110 }
1111
1112 fn on_fit_view(&mut self, _: &FitView, _: &mut Window, cx: &mut Context<Self>) {
1113 self.keyboard_nav = true;
1114 self.fit_view(cx);
1115 }
1116
1117 fn on_clear_selection(&mut self, _: &ClearSelection, _: &mut Window, cx: &mut Context<Self>) {
1118 self.keyboard_nav = true;
1119 if self.selected.is_some() {
1120 self.set_selected(None, cx);
1121 } else {
1122 cx.propagate();
1123 }
1124 }
1125
1126 fn on_activate(&mut self, _: &Activate, _: &mut Window, cx: &mut Context<Self>) {
1127 self.keyboard_nav = true;
1128 match self.selected.clone() {
1129 Some(SelectionKey::Node(id)) => {
1130 if let Some(node) = self.node(&id).cloned() {
1131 cx.emit(NetworkGraphEvent::NodeActivated(node));
1132 }
1133 }
1134 Some(SelectionKey::Group(id)) => self.toggle_group(&id, cx),
1135 _ => cx.propagate(),
1136 }
1137 }
1138
1139 fn on_open_context_menu(
1140 &mut self,
1141 _: &OpenContextMenu,
1142 _: &mut Window,
1143 cx: &mut Context<Self>,
1144 ) {
1145 self.keyboard_nav = true;
1146 let position = match self.selected_item() {
1147 Some(Item::Vertex(v)) => self.to_screen(self.vis.bodies[v].pos),
1148 Some(Item::Edge(e)) => self.edge_geometry(e).midpoint(),
1149 None => self.bounds.get().center(),
1150 };
1151 cx.emit(NetworkGraphEvent::ContextMenu {
1152 target: self.selection(),
1153 position,
1154 });
1155 }
1156
1157 fn select_toward(&mut self, dx: f32, dy: f32, cx: &mut Context<Self>) {
1160 self.keyboard_nav = true;
1161 let bodies = &self.vis.bodies;
1162 if bodies.is_empty() {
1163 return;
1164 }
1165 let from = match self.selected_item() {
1166 Some(Item::Vertex(v)) => Some(v),
1167 Some(Item::Edge(e)) => Some(self.vis.edges[e].source),
1168 None => None,
1169 };
1170 let target = match from {
1171 None => {
1172 let c = self.to_world(self.bounds.get().center());
1173 (0..bodies.len()).min_by(|&a, &b| {
1174 let d = |i: usize| (bodies[i].pos.x - c.x).hypot(bodies[i].pos.y - c.y);
1175 d(a).total_cmp(&d(b))
1176 })
1177 }
1178 Some(f) => {
1179 let p = bodies[f].pos;
1180 (0..bodies.len())
1181 .filter(|&i| i != f)
1182 .filter_map(|i| {
1183 let (x, y) = (bodies[i].pos.x - p.x, bodies[i].pos.y - p.y);
1184 let dist = x.hypot(y).max(1e-3);
1185 let cos = (x * dx + y * dy) / dist;
1186 (cos > 0.5).then_some((i, dist * (2. - cos)))
1188 })
1189 .min_by(|a, b| a.1.total_cmp(&b.1))
1190 .map(|(i, _)| i)
1191 }
1192 };
1193 let Some(v) = target else { return };
1194 self.reveal(v);
1195 let key = self.key_of(Item::Vertex(v));
1196 self.set_selected(Some(key), cx);
1197 cx.notify();
1198 }
1199
1200 fn reveal(&mut self, v: usize) {
1202 let bounds = self.bounds.get();
1203 let margin = px(48.);
1204 let p = self.to_screen(self.vis.bodies[v].pos);
1205 let inner = Bounds::from_corners(
1206 bounds.origin + point(margin, margin),
1207 bounds.bottom_right() - point(margin, margin),
1208 );
1209 if !inner.contains(&p) {
1210 self.auto_fit = false;
1211 let clamped = point(
1212 p.x.clamp(inner.left(), inner.right()),
1213 p.y.clamp(inner.top(), inner.bottom()),
1214 );
1215 self.pan += clamped - p;
1216 }
1217 }
1218
1219 fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1222 window.focus(&self.focus_handle, cx);
1223 self.keyboard_nav = false;
1224 let hit = self.hit_test(ev.position);
1225 if ev.button == MouseButton::Right {
1226 if let Some(item) = hit {
1227 self.set_selected(Some(self.key_of(item)), cx);
1228 }
1229 let target = hit.map_or(Selection::None, |item| self.item_selection(item));
1230 cx.emit(NetworkGraphEvent::ContextMenu {
1231 target,
1232 position: ev.position,
1233 });
1234 return;
1235 }
1236 if ev.click_count >= 2 {
1237 if let Some(Item::Vertex(v)) = hit {
1238 match self.vis.vertices[v] {
1239 Vertex::Node(i) => {
1240 cx.emit(NetworkGraphEvent::NodeActivated(self.nodes[i].clone()))
1241 }
1242 Vertex::Group(g) => {
1243 let id = self.groups[g].id.clone();
1244 self.expand_group(&id, cx);
1245 }
1246 }
1247 }
1248 return;
1249 }
1250 let kind = match hit {
1251 Some(Item::Vertex(ix)) => DragKind::Vertex {
1252 ix,
1253 was_pinned: self.vis.bodies[ix].fixed.is_some(),
1254 },
1255 _ => DragKind::Pan {
1256 start_pan: self.pan,
1257 },
1258 };
1259 self.drag = Some(Drag {
1260 kind,
1261 start: ev.position,
1262 moved: false,
1263 });
1264 }
1265
1266 fn on_mouse_move(&mut self, ev: &MouseMoveEvent, inside: bool, cx: &mut Context<Self>) {
1267 self.pointer = inside.then_some(ev.position);
1268 if let Some(drag) = &mut self.drag {
1269 let delta = ev.position - drag.start;
1270 if !drag.moved {
1271 if delta.x.as_f32().hypot(delta.y.as_f32()) < DRAG_THRESHOLD {
1272 return;
1273 }
1274 drag.moved = true;
1275 }
1276 match drag.kind {
1277 DragKind::Pan { start_pan } => {
1278 self.auto_fit = false;
1279 self.pan = start_pan + delta;
1280 }
1281 DragKind::Vertex { ix, .. } => {
1282 let w = self.to_world(ev.position);
1283 self.vis.bodies[ix].fixed = Some(w);
1284 self.vis.bodies[ix].pos = w;
1285 self.sim.alpha_target = 0.3;
1286 self.sim.reheat(0.3);
1287 self.auto_fit = false;
1288 }
1289 }
1290 cx.notify();
1291 return;
1292 }
1293 let hovered = if inside {
1294 self.hit_test(ev.position)
1295 } else {
1296 None
1297 };
1298 if hovered != self.hovered || hovered.is_some() {
1299 self.hovered = hovered;
1301 cx.notify();
1302 }
1303 }
1304
1305 fn on_mouse_up(&mut self, ev: &MouseUpEvent, cx: &mut Context<Self>) {
1306 let Some(drag) = self.drag.take() else { return };
1307 match drag.kind {
1308 DragKind::Vertex { ix, was_pinned } => {
1309 self.sim.alpha_target = 0.;
1310 if !drag.moved {
1311 let key = self.key_of(Item::Vertex(ix));
1312 self.set_selected(Some(key), cx);
1313 } else if !(self.sticky_drag || was_pinned) {
1314 self.vis.bodies[ix].fixed = None;
1315 }
1316 }
1317 DragKind::Pan { .. } => {
1318 if !drag.moved {
1319 let key = self.hit_test(ev.position).map(|item| self.key_of(item));
1320 self.set_selected(key, cx);
1321 }
1322 }
1323 }
1324 cx.notify();
1325 }
1326
1327 fn on_scroll(&mut self, ev: &ScrollWheelEvent, cx: &mut Context<Self>) {
1328 let delta = ev.delta.pixel_delta(px(20.));
1329 let factor = (-delta.y.as_f32() * 0.0025).exp();
1330 self.auto_fit = false;
1331 self.zoom_around(ev.position, factor);
1332 cx.notify();
1333 }
1334}
1335
1336impl Render for NetworkGraph {
1337 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1338 if self.sim.is_active() && !self.vis.bodies.is_empty() {
1339 let reduce_motion = cx.reduce_motion();
1340 let vis = &mut self.vis;
1341 if reduce_motion {
1342 let deadline = Instant::now() + REDUCED_MOTION_BUDGET;
1344 while self.sim.is_active()
1345 && self.sim.alpha_target == 0.
1346 && Instant::now() < deadline
1347 {
1348 self.sim
1349 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1350 }
1351 if self.sim.alpha_target != 0. {
1352 self.sim
1353 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1354 }
1355 } else {
1356 let ticks = if vis.bodies.len() > 1500 { 1 } else { 2 };
1358 for _ in 0..ticks {
1359 self.sim
1360 .tick(&mut vis.bodies, &vis.links, &vis.degrees, &vis.clusters);
1361 }
1362 }
1363 if self.auto_fit {
1364 self.apply_fit(if reduce_motion { 1. } else { 0.25 });
1365 }
1366 if self.sim.is_active() {
1367 window.request_animation_frame();
1368 }
1369 }
1370
1371 let colors = self.resolved_colors(cx);
1372 let metrics = GraphMetrics::from_theme(cx);
1373 let tooltip = self.tooltip_element(&colors, &metrics, window, cx);
1374 let entity = cx.entity();
1375 let bounds_cell = self.bounds.clone();
1376 let background = colors.background;
1377 let show_focus = self.keyboard_nav && self.focus_handle.is_focused(window);
1378
1379 div()
1380 .id("network-graph")
1381 .track_focus(&self.focus_handle)
1382 .key_context(KEY_CONTEXT)
1383 .on_action(cx.listener(Self::on_zoom_in))
1384 .on_action(cx.listener(Self::on_zoom_out))
1385 .on_action(cx.listener(Self::on_fit_view))
1386 .on_action(cx.listener(Self::on_clear_selection))
1387 .on_action(cx.listener(Self::on_activate))
1388 .on_action(cx.listener(Self::on_open_context_menu))
1389 .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.select_toward(-1., 0., cx)))
1390 .on_action(cx.listener(|this, _: &SelectRight, _, cx| this.select_toward(1., 0., cx)))
1391 .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.select_toward(0., -1., cx)))
1392 .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.select_toward(0., 1., cx)))
1393 .size_full()
1394 .relative()
1395 .overflow_hidden()
1396 .bg(background)
1397 .child(
1398 canvas(
1399 move |bounds, window, _| {
1400 bounds_cell.set(bounds);
1401 window.insert_hitbox(bounds, HitboxBehavior::Normal)
1402 },
1403 move |bounds, hitbox: Hitbox, window, cx| {
1404 register_input(&entity, &hitbox, window, cx);
1405 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1406 entity.update(cx, |this, cx| {
1407 this.paint_graph(bounds, &colors, &metrics, show_focus, window, cx)
1408 });
1409 });
1410 },
1411 )
1412 .size_full(),
1413 )
1414 .children(tooltip)
1415 }
1416}
1417
1418fn register_input(
1421 entity: &Entity<NetworkGraph>,
1422 hitbox: &Hitbox,
1423 window: &mut Window,
1424 cx: &mut App,
1425) {
1426 let graph = entity.read(cx);
1427 let cursor = match &graph.drag {
1429 Some(d) if d.moved => CursorStyle::ClosedHand,
1430 _ => CursorStyle::Arrow,
1431 };
1432 window.set_cursor_style(cursor, hitbox);
1433
1434 let (e, h) = (entity.clone(), hitbox.clone());
1435 window.on_mouse_event(move |ev: &MouseDownEvent, phase, window, cx| {
1436 let button_ok = matches!(ev.button, MouseButton::Left | MouseButton::Right);
1437 if phase == DispatchPhase::Bubble && button_ok && h.is_hovered(window) {
1438 e.update(cx, |this, cx| this.on_mouse_down(ev, window, cx));
1439 }
1440 });
1441 let (e, h) = (entity.clone(), hitbox.clone());
1442 window.on_mouse_event(move |ev: &MouseMoveEvent, phase, window, cx| {
1443 if phase != DispatchPhase::Bubble {
1444 return;
1445 }
1446 let inside = h.is_hovered(window);
1447 let graph = e.read(cx);
1448 if inside || graph.drag.is_some() || graph.hovered.is_some() {
1449 e.update(cx, |this, cx| this.on_mouse_move(ev, inside, cx));
1450 }
1451 });
1452 let e = entity.clone();
1453 window.on_mouse_event(move |ev: &MouseUpEvent, phase, _, cx| {
1454 if phase == DispatchPhase::Bubble && ev.button == MouseButton::Left {
1455 e.update(cx, |this, cx| this.on_mouse_up(ev, cx));
1456 }
1457 });
1458 let (e, h) = (entity.clone(), hitbox.clone());
1459 window.on_mouse_event(move |ev: &ScrollWheelEvent, phase, window, cx| {
1460 if phase == DispatchPhase::Bubble && h.should_handle_scroll(window) {
1461 e.update(cx, |this, cx| this.on_scroll(ev, cx));
1462 }
1463 });
1464}