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